diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index b76faaf9..1b09e528 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -206,6 +206,8 @@ jobs: - metadata-echo - metadata-mangabaka - metadata-openlibrary + - recommendations-anilist + - sync-anilist steps: - uses: actions/checkout@v4 - name: Setup Node.js @@ -779,6 +781,8 @@ jobs: - metadata-echo - metadata-mangabaka - metadata-openlibrary + - recommendations-anilist + - sync-anilist steps: - uses: actions/checkout@v4 - name: Setup Node.js diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index f141f7b8..cfcaf38f 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -172,6 +172,8 @@ jobs: - metadata-echo - metadata-mangabaka - metadata-openlibrary + - recommendations-anilist + - sync-anilist steps: - uses: actions/checkout@v4 - name: Setup Node.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 950fd247..48601ba5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -231,6 +231,8 @@ jobs: - metadata-echo - metadata-mangabaka - metadata-openlibrary + - recommendations-anilist + - sync-anilist steps: - name: Install base dependencies run: | @@ -832,6 +834,8 @@ jobs: - metadata-echo - metadata-mangabaka - metadata-openlibrary + - recommendations-anilist + - sync-anilist steps: - name: Install base dependencies run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42100aad..2dd09208 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,8 @@ jobs: - metadata-echo - metadata-mangabaka - metadata-openlibrary + - recommendations-anilist + - sync-anilist steps: - name: Install base dependencies run: | diff --git a/Cargo.toml b/Cargo.toml index f71fc282..2ebb108f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,9 +126,10 @@ lettre = { version = "0.11", default-features = false, features = [ "builder", ] } -# HTTP Client (for plugin cover downloads) +# HTTP Client (for plugin cover downloads and OAuth token exchange) reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", + "json", ] } # API Documentation diff --git a/Makefile b/Makefile index ded1ddc7..2e47ceee 100644 --- a/Makefile +++ b/Makefile @@ -146,6 +146,9 @@ test-fast-postgres-run: ## Run PostgreSQL tests with nextest (assumes DB running docs-install: ## Install documentation dependencies cd docs && npm install +docs-outdated: ## Check for outdated documentation dependencies + cd docs && npm outdated + docs-start: ## Start documentation dev server cd docs && npm start @@ -215,6 +218,9 @@ frontend-mock-fresh: openapi-all ## Regenerate API types, then start frontend wi frontend-install: ## Install frontend dependencies cd web && npm install +frontend-outdated: ## Check for outdated frontend dependencies + cd web && npm outdated + openapi: ## Generate OpenAPI spec from backend cargo run -- openapi --output web/openapi.json @echo "$(GREEN)OpenAPI spec generated!$(NC)" @@ -241,7 +247,7 @@ frontend-lint-fix: ## Run frontend lint with auto-fix # Plugin Development # ============================================================================= -PLUGIN_DIRS := sdk-typescript metadata-echo metadata-mangabaka metadata-openlibrary +PLUGIN_DIRS := $(notdir $(patsubst %/package.json,%,$(wildcard plugins/*/package.json))) plugins-install: ## Install dependencies for all plugins @echo "$(BLUE)Installing plugin dependencies...$(NC)" @@ -251,6 +257,14 @@ plugins-install: ## Install dependencies for all plugins done @echo "$(GREEN)All plugin dependencies installed!$(NC)" +plugins-outdated: ## Check for outdated plugin dependencies + @echo "$(BLUE)Checking for outdated plugin dependencies...$(NC)" + @for dir in $(PLUGIN_DIRS); do \ + echo "$(YELLOW)Checking $$dir...$(NC)"; \ + (cd plugins/$$dir && npm outdated); \ + done + @echo "$(GREEN)All plugin dependencies up to date!$(NC)" + plugins-build: ## Build all plugins @echo "$(BLUE)Building plugins...$(NC)" @for dir in $(PLUGIN_DIRS); do \ diff --git a/docker-compose.yml b/docker-compose.yml index ead71ec5..253c2f2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,6 +92,8 @@ services: - ./plugins/metadata-mangabaka/dist:/opt/codex/plugins/metadata-mangabaka/dist:ro - ./plugins/metadata-echo/dist:/opt/codex/plugins/metadata-echo/dist:ro - ./plugins/metadata-openlibrary/dist:/opt/codex/plugins/metadata-openlibrary/dist:ro + - ./plugins/recommendations-anilist/dist:/opt/codex/plugins/recommendations-anilist/dist:ro + - ./plugins/sync-anilist/dist:/opt/codex/plugins/sync-anilist/dist:ro environment: RUST_BACKTRACE: 1 # Email configuration for Mailhog @@ -102,6 +104,9 @@ services: CODEX_ENCRYPTION_KEY: "pjImnrzPzSmuvBKkzWAlTzrfyZ9O3pU/9IKuRT94Y/w=" # Disable workers in web container (workers run in separate container) CODEX_DISABLE_WORKERS: "true" + # OAuth redirect URI base (must match the externally-facing URL users see in their browser) + # In dev, the Vite frontend on :5173 proxies API calls to the backend + CODEX_AUTH_OIDC_REDIRECT_URI_BASE: "http://localhost:5173" # Configuration overrides (optional - uses CODEX_ prefix) # Uncomment and modify as needed to override config.docker.yaml values # Database connection @@ -148,6 +153,8 @@ services: - ./plugins/metadata-mangabaka/dist:/opt/codex/plugins/metadata-mangabaka/dist:ro - ./plugins/metadata-echo/dist:/opt/codex/plugins/metadata-echo/dist:ro - ./plugins/metadata-openlibrary/dist:/opt/codex/plugins/metadata-openlibrary/dist:ro + - ./plugins/recommendations-anilist/dist:/opt/codex/plugins/recommendations-anilist/dist:ro + - ./plugins/sync-anilist/dist:/opt/codex/plugins/sync-anilist/dist:ro command: [ "cargo", @@ -207,6 +214,8 @@ services: - /plugins/metadata-echo/node_modules - /plugins/metadata-mangabaka/node_modules - /plugins/metadata-openlibrary/node_modules + - /plugins/recommendations-anilist/node_modules + - /plugins/sync-anilist/node_modules command: - sh - -c @@ -216,13 +225,17 @@ services: cd /plugins/metadata-echo && npm install && npm run build && cd /plugins/metadata-mangabaka && npm install && npm run build && cd /plugins/metadata-openlibrary && npm install && npm run build && + cd /plugins/recommendations-anilist && npm install && npm run build && + cd /plugins/sync-anilist && npm install && npm run build && echo 'Initial build complete. Watching for changes...' && npm install -g concurrently && - concurrently --names 'sdk,metadata-echo,metadata-mangabaka' --prefix-colors 'blue,green,yellow' \ + concurrently --names 'sdk,echo,mangabaka,openlibrary,rec-anilist,sync-anilist' --prefix-colors 'blue,green,yellow,magenta,cyan,red' \ "cd /plugins/sdk-typescript && npm run dev" \ "cd /plugins/metadata-echo && npm run dev" \ "cd /plugins/metadata-mangabaka && npm run dev" \ - "cd /plugins/metadata-openlibrary && npm run dev" + "cd /plugins/metadata-openlibrary && npm run dev" \ + "cd /plugins/recommendations-anilist && npm run dev" \ + "cd /plugins/sync-anilist && npm run dev" networks: - codex-network profiles: diff --git a/docs/api/openapi.json b/docs/api/openapi.json index e0f52f89..96e4076a 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -11439,6 +11439,457 @@ ] } }, + "/api/v1/user/plugins": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "List user's plugins (enabled and available)", + "description": "Returns both plugins the user has enabled and plugins available for them to enable.", + "operationId": "list_user_plugins", + "responses": { + "200": { + "description": "User plugins list", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginsListResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + } + } + } + }, + "/api/v1/user/plugins/oauth/callback": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "Handle OAuth callback from external provider", + "description": "This endpoint receives the callback after the user authenticates with the\nexternal service. It exchanges the authorization code for tokens and stores\nthem encrypted in the database.", + "operationId": "oauth_callback", + "parameters": [ + { + "name": "code", + "in": "query", + "description": "Authorization code from OAuth provider", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "description": "State parameter for CSRF protection", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page that auto-closes the popup" + }, + "400": { + "description": "Invalid callback parameters" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "Get a single user plugin instance", + "description": "Returns detailed status for a plugin the user has enabled.", + "operationId": "get_user_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User plugin details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + }, + "delete": { + "tags": [ + "User Plugins" + ], + "summary": "Disconnect a plugin (remove data and credentials)", + "operationId": "disconnect_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to disconnect", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin disconnected and data removed" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/config": { + "patch": { + "tags": [ + "User Plugins" + ], + "summary": "Update user plugin configuration", + "description": "Allows the user to set per-user configuration overrides for their plugin instance.", + "operationId": "update_user_plugin_config", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to update config for", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserPluginConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Configuration updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "400": { + "description": "Invalid configuration" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/credentials": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Set user credentials (personal access token) for a plugin", + "description": "Allows users to authenticate by pasting a personal access token\ninstead of going through the OAuth flow.", + "operationId": "set_user_credentials", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to set credentials for", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetUserCredentialsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Credentials stored", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/disable": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Disable a plugin for the current user", + "operationId": "disable_user_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to disable", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin disabled" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/enable": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Enable a plugin for the current user", + "operationId": "enable_user_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to enable", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin enabled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "400": { + "description": "Plugin is not a user plugin or not available" + }, + "401": { + "description": "Not authenticated" + }, + "409": { + "description": "Plugin already enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/oauth/start": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Start OAuth flow for a user plugin", + "description": "Generates an authorization URL and returns it to the client.\nThe client should open this URL in a popup or redirect the user.", + "operationId": "oauth_start", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to start OAuth for", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OAuth authorization URL generated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthStartResponse" + } + } + } + }, + "400": { + "description": "Plugin does not support OAuth or not configured" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not found or not enabled" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/sync": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Trigger a sync operation for a user plugin", + "description": "Enqueues a background sync task that will push/pull reading progress\nbetween Codex and the external service.", + "operationId": "trigger_sync", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to sync", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Sync task enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncTriggerResponse" + } + } + } + }, + "400": { + "description": "Plugin is not a sync provider or not connected" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + }, + "409": { + "description": "Sync already in progress" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/sync/status": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "Get sync status for a user plugin", + "description": "Returns the current sync status including last sync time, health, and failure count.\nPass `?live=true` to also query the plugin process for live sync state (pending push/pull,\nconflicts, external entry count). This spawns the plugin process and is more expensive.", + "operationId": "get_sync_status", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to check sync status", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "live", + "in": "query", + "description": "If true, spawn the plugin process and query live sync state\n(external count, pending push/pull, conflicts).\nDefault: false (returns database-stored metadata only).", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Sync status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncStatusDto" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, "/api/v1/user/preferences": { "get": { "tags": [ @@ -11687,6 +12138,114 @@ ] } }, + "/api/v1/user/recommendations": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Get personalized recommendations", + "description": "Returns recommendations from the user's enabled recommendation plugin.\nThe plugin may return cached results or generate fresh recommendations.", + "operationId": "get_recommendations", + "responses": { + "200": { + "description": "Personalized recommendations", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecommendationsResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "No recommendation plugin enabled" + } + } + } + }, + "/api/v1/user/recommendations/refresh": { + "post": { + "tags": [ + "Recommendations" + ], + "summary": "Refresh recommendations", + "description": "Enqueues a background task to regenerate recommendations by clearing\nthe cache and updating the taste profile.", + "operationId": "refresh_recommendations", + "responses": { + "200": { + "description": "Refresh task enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecommendationsRefreshResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "No recommendation plugin enabled" + }, + "409": { + "description": "Recommendation refresh already in progress" + } + } + } + }, + "/api/v1/user/recommendations/{external_id}/dismiss": { + "post": { + "tags": [ + "Recommendations" + ], + "summary": "Dismiss a recommendation", + "description": "Tells the recommendation plugin that the user is not interested in a\nparticular recommendation, so it can be excluded from future results.", + "operationId": "dismiss_recommendation", + "parameters": [ + { + "name": "external_id", + "in": "path", + "description": "External ID of the recommendation to dismiss", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissRecommendationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Recommendation dismissed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissRecommendationResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "No recommendation plugin enabled" + } + } + } + }, "/api/v1/user/sharing-tags": { "get": { "tags": [ @@ -15107,6 +15666,59 @@ } } }, + "AvailablePluginDto": { + "type": "object", + "description": "Available plugin (not yet enabled by user)", + "required": [ + "pluginId", + "name", + "displayName", + "requiresOauth", + "oauthConfigured", + "capabilities" + ], + "properties": { + "capabilities": { + "$ref": "#/components/schemas/UserPluginCapabilitiesDto", + "description": "Plugin capabilities" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Plugin description" + }, + "displayName": { + "type": "string", + "description": "Plugin display name" + }, + "name": { + "type": "string", + "description": "Plugin name" + }, + "oauthConfigured": { + "type": "boolean", + "description": "Whether the admin has configured OAuth credentials (client_id set)" + }, + "pluginId": { + "type": "string", + "format": "uuid", + "description": "Plugin definition ID" + }, + "requiresOauth": { + "type": "boolean", + "description": "Whether this plugin requires OAuth authentication" + }, + "userSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "User-facing setup instructions for the plugin" + } + } + }, "BelongsTo": { "type": "object", "description": "Series membership information", @@ -18428,6 +19040,32 @@ } } }, + "DismissRecommendationRequest": { + "type": "object", + "description": "Request body for POST /api/v1/user/recommendations/:id/dismiss", + "properties": { + "reason": { + "type": [ + "string", + "null" + ], + "description": "Reason for dismissal" + } + } + }, + "DismissRecommendationResponse": { + "type": "object", + "description": "Response from POST /api/v1/user/recommendations/:id/dismiss", + "required": [ + "dismissed" + ], + "properties": { + "dismissed": { + "type": "boolean", + "description": "Whether the dismissal was recorded" + } + } + }, "DuplicateGroup": { "type": "object", "description": "A group of duplicate books", @@ -23245,6 +23883,57 @@ "smart" ] }, + "OAuthConfigDto": { + "type": "object", + "description": "OAuth 2.0 configuration from plugin manifest", + "required": [ + "authorizationUrl", + "tokenUrl", + "pkce" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "description": "OAuth 2.0 authorization endpoint URL" + }, + "pkce": { + "type": "boolean", + "description": "Whether to use PKCE (Proof Key for Code Exchange)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required OAuth scopes" + }, + "tokenUrl": { + "type": "string", + "description": "OAuth 2.0 token endpoint URL" + }, + "userInfoUrl": { + "type": [ + "string", + "null" + ], + "description": "Optional user info endpoint URL" + } + } + }, + "OAuthStartResponse": { + "type": "object", + "description": "OAuth initiation response", + "required": [ + "redirectUrl" + ], + "properties": { + "redirectUrl": { + "type": "string", + "description": "The URL to redirect the user to for OAuth authorization", + "example": "https://anilist.co/api/v2/oauth/authorize?response_type=code&client_id=..." + } + } + }, "OidcCallbackResponse": { "type": "object", "description": "Response from OIDC callback (successful authentication)\n\nThis mirrors the standard LoginResponse format for consistency.", @@ -25462,6 +26151,13 @@ "type": "object", "description": "Plugin capabilities", "properties": { + "externalIdSource": { + "type": [ + "string", + "null" + ], + "description": "External ID source for matching sync entries to series (e.g., \"api:anilist\")" + }, "metadataProvider": { "type": "array", "items": { @@ -25469,9 +26165,13 @@ }, "description": "Content types this plugin can provide metadata for (e.g., [\"series\", \"book\"])" }, - "userSyncProvider": { + "userReadSync": { "type": "boolean", "description": "Can sync user reading progress" + }, + "userRecommendationProvider": { + "type": "boolean", + "description": "Can provide personalized recommendations" } } }, @@ -25692,6 +26392,16 @@ "description": "Whether to skip search when external ID exists for this plugin", "example": true }, + "userCount": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Number of users who have enabled this plugin (only for user-type plugins)", + "example": 3, + "minimum": 0 + }, "workingDirectory": { "type": [ "string", @@ -25882,6 +26592,13 @@ "contentTypes" ], "properties": { + "adminSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "Admin-facing setup instructions (e.g., how to create OAuth app, set client ID)" + }, "author": { "type": [ "string", @@ -25933,6 +26650,17 @@ "type": "string", "description": "Unique identifier" }, + "oauth": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OAuthConfigDto", + "description": "OAuth 2.0 configuration (if plugin supports OAuth)" + } + ] + }, "protocolVersion": { "type": "string", "description": "Protocol version" @@ -25951,6 +26679,13 @@ }, "description": "Supported scopes" }, + "userSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "User-facing setup instructions (e.g., how to connect or get a personal token)" + }, "version": { "type": "string", "description": "Semantic version" @@ -26892,6 +27627,138 @@ } } }, + "RecommendationDto": { + "type": "object", + "description": "A single recommendation for the user", + "required": [ + "externalId", + "title", + "score", + "reason" + ], + "properties": { + "basedOn": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Titles that influenced this recommendation" + }, + "codexSeriesId": { + "type": [ + "string", + "null" + ], + "description": "Codex series ID if matched to an existing series" + }, + "coverUrl": { + "type": [ + "string", + "null" + ], + "description": "Cover image URL" + }, + "externalId": { + "type": "string", + "description": "External ID on the source service" + }, + "externalUrl": { + "type": [ + "string", + "null" + ], + "description": "URL to the entry on the external service" + }, + "genres": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Genres" + }, + "inLibrary": { + "type": "boolean", + "description": "Whether this series is already in the user's library" + }, + "reason": { + "type": "string", + "description": "Human-readable reason for this recommendation" + }, + "score": { + "type": "number", + "format": "double", + "description": "Confidence/relevance score (0.0 to 1.0)" + }, + "summary": { + "type": [ + "string", + "null" + ], + "description": "Summary/description" + }, + "title": { + "type": "string", + "description": "Title of the recommended series/book" + } + } + }, + "RecommendationsRefreshResponse": { + "type": "object", + "description": "Response from POST /api/v1/user/recommendations/refresh", + "required": [ + "taskId", + "message" + ], + "properties": { + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "taskId": { + "type": "string", + "format": "uuid", + "description": "Task ID for tracking the refresh operation" + } + } + }, + "RecommendationsResponse": { + "type": "object", + "description": "Response from GET /api/v1/user/recommendations", + "required": [ + "recommendations", + "pluginId", + "pluginName" + ], + "properties": { + "cached": { + "type": "boolean", + "description": "Whether these are cached results" + }, + "generatedAt": { + "type": [ + "string", + "null" + ], + "description": "When these recommendations were generated" + }, + "pluginId": { + "type": "string", + "format": "uuid", + "description": "Plugin that provided these recommendations" + }, + "pluginName": { + "type": "string", + "description": "Plugin display name" + }, + "recommendations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecommendationDto" + }, + "description": "Personalized recommendations" + } + } + }, "RegisterRequest": { "type": "object", "description": "Register request", @@ -28836,6 +29703,19 @@ } } }, + "SetUserCredentialsRequest": { + "type": "object", + "description": "Request to set user credentials (e.g., personal access token)", + "required": [ + "accessToken" + ], + "properties": { + "accessToken": { + "type": "string", + "description": "The access token or API key to store" + } + } + }, "SetUserRatingRequest": { "type": "object", "description": "Request to create or update a user's rating for a series", @@ -29166,50 +30046,186 @@ "string", "null" ], - "description": "Optional description", - "example": "Content appropriate for children" + "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" + } + } + }, + "SkippedField": { + "type": "object", + "description": "A field that was skipped during apply", + "required": [ + "field", + "reason" + ], + "properties": { + "field": { + "type": "string", + "description": "Field name" + }, + "reason": { + "type": "string", + "description": "Reason for skipping" + } + } + }, + "SmartBookConfig": { + "type": "object", + "description": "Configuration for smart book naming strategy", + "properties": { + "additionalGenericPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional patterns to consider as \"generic\" titles (beyond defaults)" + } + } + }, + "SyncStatusDto": { + "type": "object", + "description": "Sync status response for a user plugin", + "required": [ + "pluginId", + "pluginName", + "connected", + "healthStatus", + "failureCount", + "enabled" + ], + "properties": { + "conflicts": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of entries with conflicts on both sides (only with `?live=true`)", + "minimum": 0 + }, + "connected": { + "type": "boolean", + "description": "Whether the plugin is connected and ready to sync" + }, + "enabled": { + "type": "boolean", + "description": "Whether the plugin is currently enabled" + }, + "externalCount": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of entries tracked on the external service (only with `?live=true`)", + "minimum": 0 + }, + "failureCount": { + "type": "integer", + "format": "int32", + "description": "Number of consecutive failures" + }, + "healthStatus": { + "type": "string", + "description": "Health status" + }, + "lastFailureAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last failure timestamp" + }, + "lastSuccessAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last successful operation timestamp" + }, + "lastSyncAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last successful sync timestamp" + }, + "liveError": { + "type": [ + "string", + "null" + ], + "description": "Error message if `?live=true` was requested but the plugin could not be queried" }, - "id": { + "pendingPull": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of external entries that need to be pulled (only with `?live=true`)", + "minimum": 0 + }, + "pendingPush": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of local entries that need to be pushed (only with `?live=true`)", + "minimum": 0 + }, + "pluginId": { "type": "string", "format": "uuid", - "description": "Unique sharing tag identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" + "description": "Plugin ID" }, - "name": { + "pluginName": { "type": "string", - "description": "Display name of the sharing tag", - "example": "Kids Content" + "description": "Plugin name" } } }, - "SkippedField": { + "SyncStatusQuery": { "type": "object", - "description": "A field that was skipped during apply", - "required": [ - "field", - "reason" - ], + "description": "Query parameters for sync status endpoint", "properties": { - "field": { - "type": "string", - "description": "Field name" - }, - "reason": { - "type": "string", - "description": "Reason for skipping" + "live": { + "type": "boolean", + "description": "If true, spawn the plugin process and query live sync state\n(external count, pending push/pull, conflicts).\nDefault: false (returns database-stored metadata only)." } } }, - "SmartBookConfig": { + "SyncTriggerResponse": { "type": "object", - "description": "Configuration for smart book naming strategy", + "description": "Response from triggering a sync operation", + "required": [ + "taskId", + "message" + ], "properties": { - "additionalGenericPatterns": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Additional patterns to consider as \"generic\" titles (beyond defaults)" + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "taskId": { + "type": "string", + "format": "uuid", + "description": "Task ID for tracking the sync operation" } } }, @@ -30215,6 +31231,71 @@ ] } } + }, + { + "type": "object", + "description": "Clean up expired plugin storage data across all user plugins", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "cleanup_plugin_data" + ] + } + } + }, + { + "type": "object", + "description": "Sync user plugin data with external service", + "required": [ + "pluginId", + "userId", + "type" + ], + "properties": { + "pluginId": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "user_plugin_sync" + ] + }, + "userId": { + "type": "string", + "format": "uuid" + } + } + }, + { + "type": "object", + "description": "Refresh recommendations from a user plugin", + "required": [ + "pluginId", + "userId", + "type" + ], + "properties": { + "pluginId": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "user_plugin_recommendations" + ] + }, + "userId": { + "type": "string", + "format": "uuid" + } + } } ], "description": "Task types supported by the distributed task queue" @@ -31296,6 +32377,18 @@ } } }, + "UpdateUserPluginConfigRequest": { + "type": "object", + "description": "Request to update user plugin configuration", + "required": [ + "config" + ], + "properties": { + "config": { + "description": "Configuration overrides for this plugin" + } + } + }, "UpdateUserRequest": { "type": "object", "description": "Update user request", @@ -31552,6 +32645,181 @@ } } }, + "UserPluginCapabilitiesDto": { + "type": "object", + "description": "Plugin capabilities for display (user plugin context)", + "required": [ + "readSync", + "userRecommendationProvider" + ], + "properties": { + "readSync": { + "type": "boolean", + "description": "Can sync reading progress" + }, + "userRecommendationProvider": { + "type": "boolean", + "description": "Can provide recommendations" + } + } + }, + "UserPluginDto": { + "type": "object", + "description": "User plugin instance status", + "required": [ + "id", + "pluginId", + "pluginName", + "pluginDisplayName", + "pluginType", + "enabled", + "connected", + "healthStatus", + "requiresOauth", + "oauthConfigured", + "config", + "capabilities", + "createdAt" + ], + "properties": { + "capabilities": { + "$ref": "#/components/schemas/UserPluginCapabilitiesDto", + "description": "Plugin capabilities (derived from manifest)" + }, + "config": { + "description": "Per-user configuration" + }, + "connected": { + "type": "boolean", + "description": "Whether the plugin is connected (has valid credentials/OAuth)" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Created timestamp" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "User-facing description of the plugin" + }, + "enabled": { + "type": "boolean", + "description": "Whether the user has enabled this plugin" + }, + "externalAvatarUrl": { + "type": [ + "string", + "null" + ], + "description": "External service avatar URL" + }, + "externalUsername": { + "type": [ + "string", + "null" + ], + "description": "External service username (if connected via OAuth)" + }, + "healthStatus": { + "type": "string", + "description": "Health status of this user's plugin instance" + }, + "id": { + "type": "string", + "format": "uuid", + "description": "User plugin instance ID" + }, + "lastSuccessAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last successful operation timestamp" + }, + "lastSyncAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last sync timestamp" + }, + "lastSyncResult": { + "description": "Last sync result summary (stored in user_plugin_data)" + }, + "oauthConfigured": { + "type": "boolean", + "description": "Whether the admin has configured OAuth credentials (client_id set)" + }, + "pluginDisplayName": { + "type": "string", + "description": "Plugin display name for UI" + }, + "pluginId": { + "type": "string", + "format": "uuid", + "description": "Plugin definition ID" + }, + "pluginName": { + "type": "string", + "description": "Plugin display name" + }, + "pluginType": { + "type": "string", + "description": "Plugin type: \"system\" or \"user\"" + }, + "requiresOauth": { + "type": "boolean", + "description": "Whether this plugin requires OAuth authentication" + }, + "userConfigSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ConfigSchemaDto", + "description": "User-facing configuration schema (from plugin manifest)" + } + ] + }, + "userSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "User-facing setup instructions for the plugin" + } + } + }, + "UserPluginsListResponse": { + "type": "object", + "description": "User plugins list response", + "required": [ + "enabled", + "available" + ], + "properties": { + "available": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailablePluginDto" + }, + "description": "Plugins available for the user to enable" + }, + "enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserPluginDto" + }, + "description": "Plugins the user has enabled" + } + } + }, "UserPreferenceDto": { "type": "object", "description": "A single user preference", @@ -31940,6 +33208,14 @@ "name": "Plugin Actions", "description": "Plugin action discovery and execution for metadata fetching" }, + { + "name": "User Plugins", + "description": "User-facing plugin management, OAuth, and configuration" + }, + { + "name": "Recommendations", + "description": "Personalized recommendation endpoints" + }, { "name": "Metrics", "description": "Application metrics and statistics" @@ -32010,6 +33286,8 @@ "tags": [ "Users", "User Preferences", + "User Plugins", + "Recommendations", "Reading Progress" ] }, diff --git a/docs/dev/plugins/overview.md b/docs/dev/plugins/overview.md index 115395c6..d8f1a128 100644 --- a/docs/dev/plugins/overview.md +++ b/docs/dev/plugins/overview.md @@ -1,55 +1,56 @@ # Plugins Overview -Codex supports a plugin system that allows external processes to provide metadata, sync reading progress, and more. Plugins communicate with Codex via JSON-RPC 2.0 over stdio. +Codex supports a plugin system that allows external processes to provide metadata, sync reading progress, and generate recommendations. Plugins communicate with Codex via JSON-RPC 2.0 over stdio. ## What Are Plugins? Plugins are external processes that Codex spawns and communicates with. They can be written in any language (TypeScript, Python, Rust, etc.) and provide various capabilities: -- **Metadata Providers**: Search and fetch metadata from external sources like MangaBaka, AniList, ComicVine -- **Sync Providers** (coming soon): Sync reading progress with external services -- **Recommendation Providers** (coming soon): Provide personalized recommendations +- **Metadata Providers**: Search and fetch series/book metadata from external sources +- **Sync Providers**: Sync reading progress with external tracking services (AniList, MyAnimeList, etc.) +- **Recommendation Providers**: Generate personalized series recommendations based on user libraries ## Architecture ``` -┌──────────────────────────────────────────────────────────────────────────────────┐ -│ CODEX SERVER │ -├──────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌───────────────────────────────────────────────────────────────────────────┐ │ -│ │ Plugin Manager │ │ -│ │ │ │ -│ │ • Spawns plugin processes (command + args) │ │ -│ │ • Communicates via stdio/JSON-RPC │ │ -│ │ • Enforces RBAC permissions on writes │ │ -│ │ • Monitors health, auto-disables on failures │ │ -│ └─────────────────────────────────┬─────────────────────────────────────────┘ │ -│ │ │ -│ ┌──────────────────────┼─────────────────────────┐ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌────────────────────┐ ┌─────────────────┐ │ -│ │ MangaBaka │ │ Open Library │ │ Custom │ │ -│ │ Plugin │ │ Plugin │ │ Plugin │ │ -│ │ (series) │ │ (books, no key) │ │ │ │ -│ │ stdin/stdout │ │ stdin/stdout │ │ stdin/stdout │ │ -│ └──────────────┘ └────────────────────┘ └─────────────────┘ │ -└──────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ CODEX SERVER │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ Plugin Manager │ │ +│ │ │ │ +│ │ • Spawns plugin processes (command + args) │ │ +│ │ • Communicates via stdio/JSON-RPC │ │ +│ │ • Enforces RBAC permissions on writes │ │ +│ │ • Monitors health, auto-disables on failures │ │ +│ │ • Manages OAuth token refresh and storage quotas │ │ +│ └───────────────────────────┬───────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────┼──────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌────────────┐ ┌──────────────┐ ┌───────────────────┐ │ +│ │ Metadata │ │ Sync │ │ Recommendations │ │ +│ │ Plugins │ │ Plugins │ │ Plugins │ │ +│ │ stdin/out │ │ stdin/out │ │ stdin/out │ │ +│ └────────────┘ └──────────────┘ └───────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────┘ ``` ## Available Plugins ### Official Plugins -| Plugin | Package | Description | Status | -| --------------------- | ------------------------------------------- | -------------------------------------------------------- | --------- | -| MangaBaka Metadata | `@ashdev/codex-plugin-metadata-mangabaka` | Aggregated manga metadata from multiple sources | Available | -| Open Library Metadata | `@ashdev/codex-plugin-metadata-openlibrary` | Book metadata from Open Library via ISBN or title search | Available | -| Echo Metadata | `@ashdev/codex-plugin-metadata-echo` | Test plugin for development | Available | +| Plugin | Package | Type | Description | +|--------|---------|------|-------------| +| Echo Metadata | `@ashdev/codex-plugin-metadata-echo` | Metadata | Test plugin for development (series + book) | +| Open Library Metadata | `@ashdev/codex-plugin-metadata-openlibrary` | Metadata | Book metadata via ISBN or title search | +| AniList Sync | `@ashdev/codex-plugin-sync-anilist` | Sync | Bidirectional manga reading progress sync | +| AniList Recommendations | `@ashdev/codex-plugin-recommendations-anilist` | Recommendation | Personalized manga recommendations | ### Community Plugins -Coming soon! See [Writing Plugins](./writing-plugins.md) to create your own. +See [Writing Plugins](./writing-plugins.md) to create your own. ## Getting Started @@ -57,21 +58,16 @@ Coming soon! See [Writing Plugins](./writing-plugins.md) to create your own. The easiest way to run plugins is via `npx`, which downloads and runs the plugin automatically: -1. Navigate to **Admin Settings** → **Plugins** +1. Navigate to **Admin Settings** > **Plugins** 2. Click **Add Plugin** 3. Configure: - **Command**: `npx` - **Arguments** (one per line): ``` -y - @ashdev/codex-plugin-metadata-mangabaka@1.0.0 + @ashdev/codex-plugin-metadata-echo@1.9.3 ``` - Or for the Open Library plugin (no API key required): - ``` - -y - @ashdev/codex-plugin-metadata-openlibrary@1.0.0 - ``` -4. Add your credentials (API keys, etc.) — not required for Open Library +4. Add credentials if required (not needed for Echo or Open Library) 5. Click **Save** and **Enable** :::warning Arguments Format @@ -81,25 +77,24 @@ The easiest way to run plugins is via `npx`, which downloads and runs the plugin ``` -y -@ashdev/codex-plugin-metadata-mangabaka@1.0.0 +@ashdev/codex-plugin-metadata-echo@1.9.3 ``` ❌ Wrong: ``` --y @ashdev/codex-plugin-metadata-mangabaka@1.0.0 +-y @ashdev/codex-plugin-metadata-echo@1.9.3 ``` ::: ### npx Options -| Option | Arguments (one per line) | Description | -| ---------------- | ------------------------------------------------------------------------------- | ----------------------------- | -| Latest version | `-y`
`@ashdev/codex-plugin-metadata-mangabaka` | Always uses latest | -| Specific version | `-y`
`@ashdev/codex-plugin-metadata-mangabaka@1.0.0` | Pins to exact version | -| Version range | `-y`
`@ashdev/codex-plugin-metadata-mangabaka@^1.0.0` | Allows compatible updates | -| Faster startup | `-y`
`--prefer-offline`
`@ashdev/codex-plugin-metadata-mangabaka@1.0.0` | Skips version check if cached | +| Option | Arguments (one per line) | Description | +|--------|--------------------------|-------------| +| Latest version | `-y`
`@ashdev/codex-plugin-metadata-echo` | Always uses latest | +| Specific version | `-y`
`@ashdev/codex-plugin-metadata-echo@1.9.3` | Pins to exact version | +| Faster startup | `-y`
`--prefer-offline`
`@ashdev/codex-plugin-metadata-echo@1.9.3` | Skips version check if cached | **Flags explained:** @@ -115,14 +110,14 @@ Command: npx Arguments (one per line): -y --prefer-offline - @ashdev/codex-plugin-metadata-mangabaka@1.0.0 + @ashdev/codex-plugin-metadata-echo@1.9.3 ``` You can pre-warm the npx cache in your Dockerfile: ```dockerfile # Pre-cache plugin during image build -RUN npx -y @ashdev/codex-plugin-metadata-mangabaka@1.0.0 --version || true +RUN npx -y @ashdev/codex-plugin-metadata-echo@1.9.3 --version || true ``` ### Manual Installation @@ -130,22 +125,32 @@ RUN npx -y @ashdev/codex-plugin-metadata-mangabaka@1.0.0 --version || true For maximum performance, install globally and reference directly: ```bash -npm install -g @ashdev/codex-plugin-metadata-mangabaka +npm install -g @ashdev/codex-plugin-metadata-echo ``` Then configure: -- **Command**: `codex-plugin-metadata-mangabaka` +- **Command**: `codex-plugin-metadata-echo` - **Arguments**: (none needed) ## Plugin Lifecycle 1. **Spawn**: When a plugin is needed, Codex spawns it as a child process -2. **Initialize**: Codex sends an `initialize` request, plugin responds with its manifest -3. **Requests**: Codex sends requests (search, get, match), plugin responds +2. **Initialize**: Codex sends an `initialize` request with config, credentials, and storage handle; plugin responds with its manifest +3. **Requests**: Codex sends capability-specific requests (search, sync, recommendations); plugin responds 4. **Health Monitoring**: Failed requests are tracked; plugins auto-disable after repeated failures 5. **Shutdown**: On server shutdown or plugin disable, Codex sends `shutdown` request +## Plugin Capabilities + +| Capability | Manifest Field | Factory Function | Description | +|-----------|---------------|-----------------|-------------| +| Series Metadata | `metadataProvider: ["series"]` | `createMetadataPlugin` | Search and fetch series metadata | +| Book Metadata | `metadataProvider: ["book"]` | `createMetadataPlugin` | Search and fetch book metadata | +| Both | `metadataProvider: ["series", "book"]` | `createMetadataPlugin` | Series and book metadata | +| Read Sync | `userReadSync: true` | `createSyncPlugin` | Bidirectional reading progress sync | +| Recommendations | `userRecommendationProvider: true` | `createRecommendationPlugin` | Personalized series recommendations | + ## Advanced Configuration Codex provides advanced options to customize how plugins search for metadata and when auto-matching occurs. @@ -196,7 +201,7 @@ Control when auto-matching occurs using condition rules: "mode": "all", "rules": [ { - "field": "external_ids.plugin:mangabaka", + "field": "external_ids.plugin:metadata-echo", "operator": "is_null" }, { @@ -219,9 +224,15 @@ For detailed configuration options, see the [Preprocessing Rules Guide](/docs/pr ## Security - **RBAC**: Plugins have configurable permissions (what metadata they can write) -- **Process Isolation**: Plugins run as separate processes -- **Health Monitoring**: Failing plugins are automatically disabled -- **Credential Encryption**: API keys are encrypted at rest +- **Process Isolation**: Plugins run as separate child processes with a command allowlist and environment variable blocklist +- **Health Monitoring**: Failing plugins are automatically disabled after repeated failures +- **Credential Encryption**: API keys and OAuth tokens are encrypted at rest using AES-256-GCM +- **Data Isolation**: All plugin storage is scoped per user-plugin connection (`user_plugin_id`) +- **Request Timeouts**: JSON-RPC requests have a 30-second timeout to prevent hangs +- **OAuth Protection**: CSRF state tokens (single-use, 5-minute TTL) and PKCE S256 challenge +- **Storage Quotas**: 100 keys and 1 MB per value per user-plugin connection + +For full details, see the [Plugin Security Model](/docs/plugins#security-model) in the user documentation. ## Next Steps diff --git a/docs/dev/plugins/protocol.md b/docs/dev/plugins/protocol.md index 2dee941e..25d457e8 100644 --- a/docs/dev/plugins/protocol.md +++ b/docs/dev/plugins/protocol.md @@ -18,7 +18,7 @@ Plugins communicate with Codex via JSON-RPC 2.0 over stdio: { "jsonrpc": "2.0", "id": 1, - "method": "metadata/search", + "method": "metadata/series/search", "params": { "query": "naruto", "limit": 10 @@ -33,9 +33,7 @@ Plugins communicate with Codex via JSON-RPC 2.0 over stdio: "jsonrpc": "2.0", "id": 1, "result": { - "results": [...], - "page": 1, - "hasNextPage": true + "results": [...] } } ``` @@ -56,11 +54,11 @@ Plugins communicate with Codex via JSON-RPC 2.0 over stdio: } ``` -## Methods +## Lifecycle Methods ### initialize -Called when Codex first connects to the plugin. Returns the plugin manifest and optionally receives credentials and configuration. +Called when Codex first connects to the plugin. Sends configuration and credentials, receives the plugin manifest. **Request:** @@ -70,23 +68,20 @@ Called when Codex first connects to the plugin. Returns the plugin manifest and "id": 1, "method": "initialize", "params": { - "credentials": { - "api_key": "your-api-key" - }, - "config": { - "base_url": "https://api.example.com" - } + "adminConfig": { "maxResults": 10 }, + "userConfig": { "progressUnit": "volumes" }, + "credentials": { "access_token": "..." } } } ``` -The `params` object is optional and depends on the plugin's **credential delivery** setting: +The `params` object contains: -| Delivery Method | Value | Behavior | -| --------------------- | -------------- | ----------------------------------------------------------------------------- | -| Environment Variables | `env` | Credentials passed as env vars (e.g., `API_KEY`). No `credentials` in params. | -| Initialize Message | `init_message` | Credentials passed in `params.credentials`. | -| Both | `both` | Credentials passed both as env vars and in `params.credentials`. | +| Field | Type | Description | +|-------|------|-------------| +| `adminConfig` | `Record` | Admin-configured plugin settings | +| `userConfig` | `Record` | Per-user settings (includes `_codex` namespace for sync) | +| `credentials` | `Record` | API keys, OAuth tokens (encrypted at rest) | **Response:** @@ -102,7 +97,7 @@ The `params` object is optional and depends on the plugin's **credential deliver "author": "Your Name", "protocolVersion": "1.0", "capabilities": { - "seriesMetadataProvider": true + "metadataProvider": ["series"] } } } @@ -110,26 +105,18 @@ The `params` object is optional and depends on the plugin's **credential deliver ### ping -Health check method. Used by Codex to verify the plugin is responsive. +Health check. Used by Codex to verify the plugin is responsive. **Request:** ```json -{ - "jsonrpc": "2.0", - "id": 2, - "method": "ping" -} +{ "jsonrpc": "2.0", "id": 2, "method": "ping" } ``` **Response:** ```json -{ - "jsonrpc": "2.0", - "id": 2, - "result": "pong" -} +{ "jsonrpc": "2.0", "id": 2, "result": "pong" } ``` ### shutdown @@ -139,26 +126,22 @@ Called when Codex is shutting down or disabling the plugin. Plugins should clean **Request:** ```json -{ - "jsonrpc": "2.0", - "id": 3, - "method": "shutdown" -} +{ "jsonrpc": "2.0", "id": 3, "method": "shutdown" } ``` **Response:** ```json -{ - "jsonrpc": "2.0", - "id": 3, - "result": null -} +{ "jsonrpc": "2.0", "id": 3, "result": null } ``` -### metadata/search +## Metadata Methods + +Methods are scoped by content type: `metadata/series/*` for series, `metadata/book/*` for books. + +### metadata/series/search -Search for metadata by query string. +Search for series metadata by query string. **Request:** @@ -166,7 +149,7 @@ Search for metadata by query string. { "jsonrpc": "2.0", "id": 4, - "method": "metadata/search", + "method": "metadata/series/search", "params": { "query": "one piece", "limit": 10 @@ -185,24 +168,24 @@ Search for metadata by query string. { "externalId": "12345", "title": "One Piece", - "summary": "A pirate adventure...", + "alternateTitles": ["ワンピース"], "year": 1997, "coverUrl": "https://example.com/cover.jpg", - "status": "ongoing", - "score": 95, - "providerData": { - "url": "https://example.com/series/12345" + "relevanceScore": 0.95, + "preview": { + "status": "ongoing", + "genres": ["Action", "Adventure"], + "rating": 9.5, + "description": "A pirate adventure..." } } ], - "totalResults": 1, - "page": 1, - "hasNextPage": false + "nextCursor": null } } ``` -### metadata/get +### metadata/series/get Get full metadata for an external ID. @@ -212,10 +195,8 @@ Get full metadata for an external ID. { "jsonrpc": "2.0", "id": 5, - "method": "metadata/get", - "params": { - "externalId": "12345" - } + "method": "metadata/series/get", + "params": { "externalId": "12345" } } ``` @@ -227,30 +208,38 @@ Get full metadata for an external ID. "id": 5, "result": { "externalId": "12345", - "titles": [ - { "value": "One Piece", "language": "en", "primary": true }, - { "value": "ワンピース", "language": "ja" } + "externalUrl": "https://example.com/series/12345", + "title": "One Piece", + "alternateTitles": [ + { "title": "ワンピース", "language": "ja", "titleType": "native" }, + { "title": "Wan Piisu", "language": "ja-Latn", "titleType": "romaji" } ], "summary": "A long, epic pirate adventure...", "status": "ongoing", "year": 1997, - "coverUrl": "https://example.com/cover.jpg", + "totalBookCount": 108, + "language": "ja", + "readingDirection": "rtl", "genres": ["Action", "Adventure", "Comedy"], - "tags": ["Pirates", "Superpowers", "Long Running"], - "authors": [{ "name": "Eiichiro Oda", "role": "author" }], + "tags": ["Pirates", "Superpowers"], + "authors": ["Eiichiro Oda"], + "artists": ["Eiichiro Oda"], "publisher": "Shueisha", - "rating": 9.5, - "ratingCount": 100000, + "coverUrl": "https://example.com/cover.jpg", + "rating": { "score": 95, "voteCount": 100000, "source": "example" }, + "externalRatings": [ + { "score": 95, "voteCount": 100000, "source": "example" } + ], "externalLinks": [ - { "name": "MangaBaka", "url": "https://mangabaka.org/series/12345" } + { "url": "https://example.com/12345", "label": "Example", "linkType": "provider" } ] } } ``` -### metadata/match +### metadata/series/match -Find best match for existing content (auto-matching). +Find best match for existing content (auto-matching during library scans). **Request:** @@ -258,7 +247,7 @@ Find best match for existing content (auto-matching). { "jsonrpc": "2.0", "id": 6, - "method": "metadata/match", + "method": "metadata/series/match", "params": { "title": "One Piece", "year": 1997, @@ -277,105 +266,352 @@ Find best match for existing content (auto-matching). "match": { "externalId": "12345", "title": "One Piece", + "alternateTitles": [], "year": 1997, - "score": 95 + "relevanceScore": 0.95 }, - "confidence": 98, - "alternatives": [ + "confidence": 0.98, + "alternatives": [] + } +} +``` + +### metadata/book/search + +Search for book metadata by ISBN, query, or author. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 7, + "method": "metadata/book/search", + "params": { + "isbn": "978-0-306-40615-7", + "limit": 5 + } +} +``` + +Parameters: `isbn`, `query`, `author`, `year`, `limit`, `cursor` (all optional, at least one of `isbn`/`query` required). + +### metadata/book/get + +Get full book metadata. Same request format as `metadata/series/get`. Response includes book-specific fields: `volume`, `pageCount`, `isbn`, `isbns`, `edition`, `seriesPosition`, `authors` (with roles), `covers`, `awards`, etc. + +### metadata/book/match + +Match a book by ISBN (preferred) or title. Parameters: `title`, `isbn`, `authors`, `year`, `publisher`. + +## Sync Methods + +### sync/getUserInfo + +Get the authenticated user's profile on the external service. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 10, + "method": "sync/getUserInfo", + "params": {} +} +``` + +**Response:** + +```json +{ + "jsonrpc": "2.0", + "id": 10, + "result": { + "externalId": "42", + "username": "reader123", + "avatarUrl": "https://example.com/avatar.jpg", + "profileUrl": "https://example.com/user/42" + } +} +``` + +### sync/pushProgress + +Push local reading progress to the external service. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 11, + "method": "sync/pushProgress", + "params": { + "entries": [ { - "externalId": "67890", - "title": "One Piece: Strong World", - "score": 60 + "externalId": "12345", + "title": "One Piece", + "status": "reading", + "progress": { "chapters": 50, "volumes": 5 }, + "rating": 95, + "startedAt": "2024-01-15", + "completedAt": null, + "latestUpdatedAt": "2024-06-01T12:00:00Z" } ] } } ``` -## Data Types +**Response:** -### SearchResult - -```typescript -interface SearchResult { - externalId: string; // Provider's ID for this item - title: string; // Primary display title - alternateTitles?: Title[]; - year?: number; - coverUrl?: string; - summary?: string; - status?: "ongoing" | "completed" | "hiatus" | "cancelled" | "unknown"; - score?: number; // Relevance score (0-100) - providerData?: object; // Passed to metadata/get +```json +{ + "jsonrpc": "2.0", + "id": 11, + "result": { + "successes": ["12345"], + "failures": [] + } } ``` -### SeriesMetadata - -```typescript -interface SeriesMetadata { - externalId: string; - titles: Title[]; - summary?: string; - status?: "ongoing" | "completed" | "hiatus" | "cancelled" | "unknown"; - year?: number; - yearEnd?: number; - coverUrl?: string; - bannerUrl?: string; - genres?: string[]; - tags?: string[]; - contentRating?: string; - authors?: Person[]; - artists?: Person[]; - publisher?: string; - originalLanguage?: string; - country?: string; - rating?: number; // 0-10 scale - ratingCount?: number; - externalLinks?: ExternalLink[]; - providerData?: object; +### sync/pullProgress + +Pull reading progress from the external service. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 12, + "method": "sync/pullProgress", + "params": { + "page": 1, + "updatedSince": "2024-01-01T00:00:00Z" + } } +``` -interface Title { - value: string; - language?: string; // ISO 639-1 code - primary?: boolean; +**Response:** + +```json +{ + "jsonrpc": "2.0", + "id": 12, + "result": { + "entries": [ + { + "externalId": "12345", + "title": "One Piece", + "status": "reading", + "progress": { "chapters": 50, "volumes": 5 }, + "rating": 95, + "startedAt": "2024-01-15", + "lastReadAt": "2024-06-01", + "latestUpdatedAt": "2024-06-01T12:00:00Z" + } + ], + "hasMore": false + } +} +``` + +### sync/status (Optional) + +Return sync status summary. + +**Response:** + +```json +{ + "jsonrpc": "2.0", + "id": 13, + "result": { + "lastSyncAt": "2024-06-01T12:00:00Z", + "totalEntries": 150, + "syncedEntries": 148, + "conflicts": 2 + } +} +``` + +## Recommendation Methods + +### recommendations/get + +Generate recommendations based on the user's library. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 20, + "method": "recommendations/get", + "params": { + "library": [ + { + "seriesId": "abc", + "title": "One Piece", + "genres": ["Action", "Adventure"], + "tags": ["Pirates"], + "booksRead": 50, + "booksOwned": 108, + "userRating": 95, + "externalIds": [{ "source": "api:anilist", "externalId": "21" }] + } + ], + "limit": 20, + "excludeIds": ["67890"] + } } +``` -interface Person { - name: string; - role?: string; +**Response:** + +```json +{ + "jsonrpc": "2.0", + "id": 20, + "result": { + "recommendations": [ + { + "externalId": "99999", + "url": "https://example.com/series/99999", + "title": "Naruto", + "coverUrl": "https://example.com/naruto-cover.jpg", + "description": "A ninja's journey...", + "genres": ["Action", "Adventure"], + "rating": 0.85, + "why": "Recommended because you liked \"One Piece\"" + } + ] + } } +``` -interface ExternalLink { - name: string; - url: string; +### recommendations/dismiss (Optional) + +Dismiss a recommendation so it won't appear again. + +**Request:** + +```json +{ + "jsonrpc": "2.0", + "id": 21, + "method": "recommendations/dismiss", + "params": { + "externalId": "99999", + "reason": "not_interested" + } } ``` +Dismiss reasons: `not_interested`, `already_read`, `already_owned`. + +### recommendations/clear (Optional) + +Clear cached recommendation data. + +### recommendations/updateProfile (Optional) + +Update the user's taste profile with new library data. + +## Storage Methods (Plugin → Host) + +Plugins can send storage requests to the host (Codex) via stdout. These are JSON-RPC requests sent *from* the plugin *to* the host. + +### storage/get + +```json +{ "jsonrpc": "2.0", "id": "s1", "method": "storage/get", "params": { "key": "cache-key" } } +``` + +### storage/set + +```json +{ "jsonrpc": "2.0", "id": "s2", "method": "storage/set", "params": { "key": "cache-key", "data": {...}, "expiresAt": "2025-12-31T00:00:00Z" } } +``` + +### storage/delete + +```json +{ "jsonrpc": "2.0", "id": "s3", "method": "storage/delete", "params": { "key": "cache-key" } } +``` + +### storage/list + +```json +{ "jsonrpc": "2.0", "id": "s4", "method": "storage/list", "params": {} } +``` + +### storage/clear + +```json +{ "jsonrpc": "2.0", "id": "s5", "method": "storage/clear", "params": {} } +``` + +**Storage limits:** 100 keys per user-plugin, 1 MB per value. + +## Data Types + +### Reading Status + +``` +"reading" | "completed" | "on_hold" | "dropped" | "plan_to_read" +``` + +### Series Status + +``` +"ongoing" | "ended" | "hiatus" | "abandoned" | "unknown" +``` + +### Reading Direction + +``` +"ltr" | "rtl" | "ttb" +``` + +### External Link Types + +``` +"provider" | "official" | "social" | "purchase" | "read" | "other" +``` + +### Dismiss Reasons + +``` +"not_interested" | "already_read" | "already_owned" +``` + ## Error Codes ### Standard JSON-RPC Errors -| Code | Message | Description | -| ------ | ---------------- | ---------------------------- | -| -32700 | Parse error | Invalid JSON | -| -32600 | Invalid Request | Not a valid JSON-RPC request | -| -32601 | Method not found | Method doesn't exist | -| -32602 | Invalid params | Invalid method parameters | -| -32603 | Internal error | Internal plugin error | +| Code | Message | Description | +|------|---------|-------------| +| -32700 | Parse error | Invalid JSON | +| -32600 | Invalid Request | Not a valid JSON-RPC request | +| -32601 | Method not found | Method doesn't exist | +| -32602 | Invalid params | Invalid method parameters | +| -32603 | Internal error | Internal plugin error | ### Plugin-Specific Errors -| Code | Message | Description | -| ------ | ------------ | -------------------------- | -| -32001 | Rate limited | API rate limit exceeded | -| -32002 | Not found | Resource not found | -| -32003 | Auth failed | Authentication failed | -| -32004 | API error | External API error | +| Code | Message | Description | +|------|---------|-------------| +| -32001 | Rate limited | API rate limit exceeded | +| -32002 | Not found | Resource not found | +| -32003 | Auth failed | Authentication failed | +| -32004 | API error | External API error | | -32005 | Config error | Plugin configuration error | -## Lifecycle +## Lifecycle Diagram ``` Codex Plugin Process @@ -390,8 +626,11 @@ Codex Plugin Process │─── {"method":"ping"} ───────────────────────────────▶│ │◀─── {"result": "pong"} ──────────────────────────────│ │ │ - │─── {"method":"metadata/search",...} ────────────────▶│ - │◀─── {"result": [...]} ───────────────────────────────│ + │─── {"method":"metadata/series/search",...} ─────────▶│ + │◀─── {"result": {results: [...]}} ───────────────────│ + │ │ + │◀─── {"method":"storage/set",...} ────────────────────│ (plugin → host) + │─── {"result": {success: true}} ─────────────────────▶│ │ │ │ ... more requests ... │ │ │ @@ -403,9 +642,10 @@ Codex Plugin Process ## Best Practices -1. **Never write to stdout except for JSON-RPC responses** +1. **Never write to stdout except for JSON-RPC responses** (and storage requests) 2. **Use stderr for all logging** -3. **Handle unknown methods gracefully** - return METHOD_NOT_FOUND error -4. **Include request ID in responses** - even for errors -5. **Exit cleanly on shutdown** - clean up resources, then exit -6. **Handle malformed requests** - don't crash on bad input +3. **Handle unknown methods gracefully** — return METHOD_NOT_FOUND error +4. **Include request ID in responses** — even for errors +5. **Exit cleanly on shutdown** — clean up resources, then exit +6. **Handle malformed requests** — don't crash on bad input +7. **Ignore unknown fields** — ensures forward compatibility with protocol additions diff --git a/docs/dev/plugins/sdk.md b/docs/dev/plugins/sdk.md index b56c440b..65d751ab 100644 --- a/docs/dev/plugins/sdk.md +++ b/docs/dev/plugins/sdk.md @@ -8,13 +8,16 @@ The `@ashdev/codex-plugin-sdk` package provides TypeScript types, utilities, and npm install @ashdev/codex-plugin-sdk ``` +Requires Node.js 22+. + ## Quick Example ```typescript import { - createSeriesMetadataPlugin, - type SeriesMetadataProvider, + createMetadataPlugin, + type MetadataProvider, type PluginManifest, + type MetadataContentType, } from "@ashdev/codex-plugin-sdk"; const manifest = { @@ -24,10 +27,14 @@ const manifest = { description: "A metadata provider", author: "Your Name", protocolVersion: "1.0", - capabilities: { seriesMetadataProvider: true }, -} as const satisfies PluginManifest & { capabilities: { seriesMetadataProvider: true } }; + capabilities: { + metadataProvider: ["series"] as MetadataContentType[], + }, +} as const satisfies PluginManifest & { + capabilities: { metadataProvider: MetadataContentType[] }; +}; -const provider: SeriesMetadataProvider = { +const provider: MetadataProvider = { async search(params) { return { results: [] }; }, @@ -35,51 +42,139 @@ const provider: SeriesMetadataProvider = { return { externalId: params.externalId, externalUrl: `https://example.com/${params.externalId}`, - alternateTitles: [], - genres: [], - tags: [], - authors: [], - artists: [], - externalLinks: [], }; }, }; -createSeriesMetadataPlugin({ manifest, provider }); +createMetadataPlugin({ manifest, provider }); +``` + +## Factory Functions + +### createMetadataPlugin + +Creates a metadata plugin server for series and/or book metadata. + +```typescript +function createMetadataPlugin(options: MetadataPluginOptions): void; + +interface MetadataPluginOptions { + manifest: PluginManifest & { capabilities: { metadataProvider: MetadataContentType[] } }; + provider?: MetadataProvider; // Series metadata provider + bookProvider?: BookMetadataProvider; // Book metadata provider + onInitialize?: (params: InitializeParams) => void | Promise; + logLevel?: "debug" | "info" | "warn" | "error"; +} +``` + +Routes methods automatically: +- `metadata/series/search` → `provider.search()` +- `metadata/series/get` → `provider.get()` +- `metadata/series/match` → `provider.match()` +- `metadata/book/search` → `bookProvider.search()` +- `metadata/book/get` → `bookProvider.get()` +- `metadata/book/match` → `bookProvider.match()` + +### createSyncPlugin + +Creates a sync plugin server for reading progress synchronization. + +```typescript +function createSyncPlugin(options: SyncPluginOptions): void; + +interface SyncPluginOptions { + manifest: PluginManifest & { capabilities: { userReadSync: true } }; + provider: SyncProvider; + onInitialize?: (params: InitializeParams) => void | Promise; + logLevel?: "debug" | "info" | "warn" | "error"; +} ``` -## API Reference +Routes methods: +- `sync/getUserInfo` → `provider.getUserInfo()` +- `sync/pushProgress` → `provider.pushProgress()` +- `sync/pullProgress` → `provider.pullProgress()` +- `sync/status` → `provider.status()` -### createSeriesMetadataPlugin +### createRecommendationPlugin -Creates and starts a series metadata plugin server that handles JSON-RPC communication. +Creates a recommendation plugin server. ```typescript -function createSeriesMetadataPlugin(options: SeriesMetadataPluginOptions): void; +function createRecommendationPlugin(options: RecommendationPluginOptions): void; -interface SeriesMetadataPluginOptions { - manifest: PluginManifest & { capabilities: { seriesMetadataProvider: true } }; - provider: SeriesMetadataProvider; +interface RecommendationPluginOptions { + manifest: PluginManifest & { capabilities: { userRecommendationProvider: true } }; + provider: RecommendationProvider; onInitialize?: (params: InitializeParams) => void | Promise; logLevel?: "debug" | "info" | "warn" | "error"; } ``` -### SeriesMetadataProvider +Routes methods: +- `recommendations/get` → `provider.get()` +- `recommendations/updateProfile` → `provider.updateProfile()` +- `recommendations/clear` → `provider.clear()` +- `recommendations/dismiss` → `provider.dismiss()` + +### InitializeParams + +Passed to the `onInitialize` callback: + +```typescript +interface InitializeParams { + adminConfig?: Record; // From manifest.configSchema + userConfig?: Record; // From manifest.userConfigSchema + credentials?: Record; // From manifest.requiredCredentials + storage: PluginStorage; // Scoped storage client +} +``` + +## Provider Interfaces -Interface for implementing series metadata providers: +### MetadataProvider ```typescript -interface SeriesMetadataProvider { +interface MetadataProvider { search(params: MetadataSearchParams): Promise; get(params: MetadataGetParams): Promise; match?(params: MetadataMatchParams): Promise; } ``` -### createLogger +### BookMetadataProvider + +```typescript +interface BookMetadataProvider { + search(params: BookSearchParams): Promise; + get(params: MetadataGetParams): Promise; + match?(params: BookMatchParams): Promise; +} +``` + +### SyncProvider + +```typescript +interface SyncProvider { + getUserInfo(): Promise; + pushProgress(params: SyncPushRequest): Promise; + pullProgress(params: SyncPullRequest): Promise; + status?(): Promise; +} +``` + +### RecommendationProvider + +```typescript +interface RecommendationProvider { + get(params: RecommendationRequest): Promise; + updateProfile?(params: ProfileUpdateRequest): Promise; + clear?(): Promise; + dismiss?(params: RecommendationDismissRequest): Promise; +} +``` -Creates a logger that writes to stderr (safe for plugins). +## Logging ```typescript function createLogger(options: LoggerOptions): Logger; @@ -98,11 +193,12 @@ interface Logger { } ``` -**Example:** +Writes to **stderr** only (stdout is reserved for JSON-RPC). ```typescript -const logger = createLogger({ name: "metadata-my-plugin", level: "debug" }); +import { createLogger } from "@ashdev/codex-plugin-sdk"; +const logger = createLogger({ name: "my-plugin", level: "debug" }); logger.info("Plugin started"); logger.debug("Processing request", { params }); logger.error("Request failed", error); @@ -110,75 +206,85 @@ logger.error("Request failed", error); ## Error Classes -### RateLimitError +All error classes extend `PluginError` and automatically convert to JSON-RPC error responses. -Thrown when rate limited by an external API. +### RateLimitError ```typescript import { RateLimitError } from "@ashdev/codex-plugin-sdk"; - -if (response.status === 429) { - throw new RateLimitError(60); // Retry after 60 seconds -} +throw new RateLimitError(60); // Retry after 60 seconds +throw new RateLimitError(60, "API rate limit exceeded"); // With message +// Code: -32001 ``` ### NotFoundError -Thrown when a requested resource doesn't exist. - ```typescript import { NotFoundError } from "@ashdev/codex-plugin-sdk"; - -if (response.status === 404) { - throw new NotFoundError("Series not found"); -} +throw new NotFoundError("Series not found"); +// Code: -32002 ``` ### AuthError -Thrown when authentication fails. - ```typescript import { AuthError } from "@ashdev/codex-plugin-sdk"; - -if (response.status === 401) { - throw new AuthError("Invalid API key"); -} +throw new AuthError("Invalid API key"); +// Code: -32003 ``` ### ApiError -Thrown for generic API errors. - ```typescript import { ApiError } from "@ashdev/codex-plugin-sdk"; - -if (!response.ok) { - throw new ApiError(`API error: ${response.status}`, response.status); -} +throw new ApiError("External API returned 500", 500); +// Code: -32004 ``` ### ConfigError -Thrown when the plugin is misconfigured. - ```typescript import { ConfigError } from "@ashdev/codex-plugin-sdk"; +throw new ConfigError("api_key credential is required"); +// Code: -32005 +``` + +## Storage -if (!apiKey) { - throw new ConfigError("api_key credential is required"); +The `PluginStorage` class provides a key-value store scoped per user-plugin connection. + +```typescript +class PluginStorage { + async get(key: string): Promise; + async set(key: string, data: unknown, expiresAt?: string): Promise; + async delete(key: string): Promise; + async list(): Promise; + async clear(): Promise; } ``` +**Response types:** + +```typescript +interface StorageGetResponse { data: unknown | null; expiresAt?: string; } +interface StorageSetResponse { success: boolean; } +interface StorageDeleteResponse { deleted: boolean; } +interface StorageListResponse { keys: StorageKeyEntry[]; } +interface StorageClearResponse { deletedCount: number; } +interface StorageKeyEntry { key: string; expiresAt?: string; updatedAt: string; } +``` + +**Limits:** 100 keys per user-plugin, 1 MB per value. + ## Types -### PluginManifest +### Manifest ```typescript interface PluginManifest { - name: string; // Unique identifier (e.g., "metadata-myplugin") - displayName: string; - version: string; + name: string; // Unique ID (lowercase, alphanumeric, hyphens) + displayName: string; // User-facing name + version: string; // Semver description: string; author: string; homepage?: string; @@ -186,12 +292,53 @@ interface PluginManifest { protocolVersion: "1.0"; capabilities: PluginCapabilities; requiredCredentials?: CredentialField[]; + configSchema?: ConfigSchema; // Admin settings + userConfigSchema?: ConfigSchema; // Per-user settings + oauth?: OAuthConfig; // OAuth 2.0 configuration + userDescription?: string; + adminSetupInstructions?: string; + userSetupInstructions?: string; } interface PluginCapabilities { - seriesMetadataProvider?: boolean; - syncProvider?: boolean; - recommendationProvider?: boolean; + metadataProvider?: MetadataContentType[]; // "series" and/or "book" + userReadSync?: boolean; + externalIdSource?: string; // e.g., "api:anilist" + userRecommendationProvider?: boolean; +} + +type MetadataContentType = "series" | "book"; +``` + +### OAuth + +```typescript +interface OAuthConfig { + authorizationUrl: string; + tokenUrl: string; + scopes?: string[]; + pkce?: boolean; // Default: true + userInfoUrl?: string; + clientId?: string; // Default client ID +} +``` + +### Config & Credentials + +```typescript +interface ConfigSchema { + description: string; + fields: ConfigField[]; +} + +interface ConfigField { + key: string; + label: string; + description?: string; + type: "string" | "number" | "boolean"; + required?: boolean; + default?: unknown; + example?: unknown; } interface CredentialField { @@ -205,7 +352,7 @@ interface CredentialField { } ``` -### MetadataSearchParams / MetadataSearchResponse +### Metadata Search ```typescript interface MetadataSearchParams { @@ -225,7 +372,7 @@ interface SearchResult { alternateTitles: string[]; year?: number; coverUrl?: string; - relevanceScore: number; // 0.0-1.0 + relevanceScore: number; // 0.0-1.0 preview?: SearchResultPreview; } @@ -234,21 +381,19 @@ interface SearchResultPreview { genres?: string[]; rating?: number; description?: string; + bookCount?: number; + authors?: string[]; } ``` -### MetadataGetParams / PluginSeriesMetadata +### Series Metadata ```typescript -interface MetadataGetParams { - externalId: string; -} - interface PluginSeriesMetadata { externalId: string; externalUrl?: string; title?: string; - alternateTitles: AlternateTitle[]; + alternateTitles?: AlternateTitle[]; summary?: string; status?: SeriesStatus; year?: number; @@ -256,29 +401,79 @@ interface PluginSeriesMetadata { language?: string; ageRating?: number; readingDirection?: ReadingDirection; - genres: string[]; - tags: string[]; - authors: string[]; - artists: string[]; + genres?: string[]; + tags?: string[]; + authors?: string[]; + artists?: string[]; publisher?: string; coverUrl?: string; bannerUrl?: string; rating?: ExternalRating; externalRatings?: ExternalRating[]; - externalLinks: ExternalLink[]; + externalLinks?: ExternalLink[]; } -interface AlternateTitle { - title: string; +type SeriesStatus = "ongoing" | "ended" | "hiatus" | "abandoned" | "unknown"; +type ReadingDirection = "ltr" | "rtl" | "ttb"; +``` + +### Book Metadata + +```typescript +interface PluginBookMetadata { + externalId: string; + externalUrl?: string; + title?: string; + subtitle?: string; + alternateTitles?: AlternateTitle[]; + summary?: string; + bookType?: string; + volume?: number; + pageCount?: number; + releaseDate?: string; + year?: number; + isbn?: string; + isbns?: string[]; + edition?: string; + originalTitle?: string; + originalYear?: number; + translator?: string; language?: string; - titleType?: "english" | "native" | "romaji" | string; + seriesPosition?: number; + seriesTotal?: number; + genres?: string[]; + tags?: string[]; + subjects?: string[]; + authors?: BookAuthor[]; + artists?: string[]; + publisher?: string; + coverUrl?: string; + covers?: BookCover[]; + rating?: ExternalRating; + externalRatings?: ExternalRating[]; + awards?: BookAward[]; + externalLinks?: ExternalLink[]; +} + +interface BookSearchParams { + isbn?: string; + query?: string; + author?: string; + year?: number; + limit?: number; + cursor?: string; +} + +interface BookAuthor { + name: string; + role?: BookAuthorRole; + sortName?: string; } -type SeriesStatus = "ongoing" | "ended" | "cancelled" | "hiatus" | "unknown"; -type ReadingDirection = "ltr" | "rtl" | "ttb" | "btt"; +type BookAuthorRole = "author" | "coauthor" | "editor" | "translator" | "illustrator" | "contributor"; ``` -### MetadataMatchParams / MetadataMatchResponse +### Matching ```typescript interface MetadataMatchParams { @@ -287,18 +482,135 @@ interface MetadataMatchParams { author?: string; } +interface BookMatchParams { + title: string; + authors?: string[]; + isbn?: string; + year?: number; + publisher?: string; +} + interface MetadataMatchResponse { match: SearchResult | null; - confidence: number; // 0.0-1.0 + confidence: number; // 0.0-1.0 alternatives?: SearchResult[]; } ``` +### Sync Types + +```typescript +type SyncReadingStatus = "reading" | "completed" | "on_hold" | "dropped" | "plan_to_read"; + +interface ExternalUserInfo { + externalId: string; + username: string; + avatarUrl?: string; + profileUrl?: string; +} + +interface SyncProgress { + chapters?: number; + volumes?: number; + pages?: number; + totalChapters?: number; + totalVolumes?: number; +} + +interface SyncEntry { + externalId: string; + title?: string; + status: SyncReadingStatus; + progress?: SyncProgress; + rating?: number; // 0-100 + startedAt?: string; + lastReadAt?: string; + completedAt?: string; + latestUpdatedAt?: string; // For staleness detection +} + +interface SyncPushRequest { entries: SyncEntry[]; } +interface SyncPushResponse { + successes: string[]; + failures: Array<{ externalId: string; error: string }>; +} + +interface SyncPullRequest { + page?: number; + updatedSince?: string; + limit?: number; + cursor?: string; +} +interface SyncPullResponse { + entries: SyncEntry[]; + hasMore: boolean; + nextPage?: number; + nextCursor?: string; +} + +interface SyncStatusResponse { + lastSyncAt?: string; + totalEntries?: number; + syncedEntries?: number; + conflicts?: number; +} +``` + +### Recommendation Types + +```typescript +type DismissReason = "not_interested" | "already_read" | "already_owned"; + +interface UserLibraryEntry { + seriesId: string; + title: string; + genres?: string[]; + tags?: string[]; + booksRead?: number; + booksOwned?: number; + userRating?: number; // 0-100 + externalIds?: ExternalId[]; +} + +interface RecommendationRequest { + library: UserLibraryEntry[]; + limit?: number; + excludeIds?: string[]; +} + +interface Recommendation { + externalId: string; + url?: string; + title: string; + coverUrl?: string; + description?: string; + genres?: string[]; + rating?: number; // 0.0-1.0 + why?: string; + basedOn?: Array<{ title: string; externalId: string }>; +} + +interface RecommendationResponse { + recommendations: Recommendation[]; +} + +interface RecommendationDismissRequest { + externalId: string; + reason?: DismissReason; +} +``` + ### Supporting Types ```typescript +interface AlternateTitle { + title: string; + language?: string; + titleType?: "english" | "native" | "romaji" | string; +} + interface ExternalRating { - score: number; // 0-100 + score: number; // 0-100 voteCount?: number; source: string; } @@ -309,16 +621,27 @@ interface ExternalLink { linkType?: ExternalLinkType; } -type ExternalLinkType = - | "provider" - | "official" - | "social" - | "purchase" - | "info" - | "other"; +type ExternalLinkType = "provider" | "official" | "social" | "purchase" | "read" | "other"; + +interface ExternalId { + source: string; // e.g., "api:anilist" + externalId: string; +} + +interface BookCover { url: string; width?: number; height?: number; size?: BookCoverSize; } +type BookCoverSize = "small" | "medium" | "large"; + +interface BookAward { name: string; year?: number; category?: string; won?: boolean; } ``` -## JSON-RPC Types +### Constants + +```typescript +import { EXTERNAL_ID_SOURCE_ANILIST } from "@ashdev/codex-plugin-sdk"; +// Value: "api:anilist" +``` + +### JSON-RPC Types ```typescript interface JsonRpcRequest { @@ -345,12 +668,8 @@ interface JsonRpcError { message: string; data?: unknown; } -``` -## Error Codes - -```typescript -// Standard JSON-RPC errors +// Standard JSON-RPC error codes const JSON_RPC_ERROR_CODES = { PARSE_ERROR: -32700, INVALID_REQUEST: -32600, @@ -359,7 +678,7 @@ const JSON_RPC_ERROR_CODES = { INTERNAL_ERROR: -32603, }; -// Plugin-specific errors +// Plugin-specific error codes const PLUGIN_ERROR_CODES = { RATE_LIMITED: -32001, NOT_FOUND: -32002, @@ -368,22 +687,3 @@ const PLUGIN_ERROR_CODES = { CONFIG_ERROR: -32005, }; ``` - -## Initialize Callback - -Use `onInitialize` to receive credentials and configuration: - -```typescript -createSeriesMetadataPlugin({ - manifest, - provider, - onInitialize(params) { - // params.credentials - Credential values (e.g., { api_key: "..." }) - // params.config - Configuration values - if (!params.credentials?.api_key) { - throw new ConfigError("api_key credential is required"); - } - apiKey = params.credentials.api_key; - }, -}); -``` diff --git a/docs/dev/plugins/writing-plugins.md b/docs/dev/plugins/writing-plugins.md index a1a4c370..ebc8c61b 100644 --- a/docs/dev/plugins/writing-plugins.md +++ b/docs/dev/plugins/writing-plugins.md @@ -1,31 +1,62 @@ # Writing Plugins -This guide walks you through creating a Codex metadata plugin from scratch using TypeScript and the official SDK. +This guide walks you through building your own Codex plugin — from a simple metadata provider to sync and recommendation plugins that integrate with external services. ## Prerequisites -- Node.js 18 or later -- npm or pnpm -- Basic TypeScript knowledge +- **Node.js 22+** — plugins run as child processes launched by Codex +- **TypeScript 5.7+** — recommended for type safety; the SDK provides full type definitions +- **npm** or a compatible package manager -## Quick Start +## Plugin Architecture Overview -### 1. Create a New Project +Codex plugins are standalone processes that communicate with the Codex server over **stdin/stdout** using the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) protocol. The SDK handles all protocol details — you implement provider interfaces and the SDK takes care of message routing, error formatting, and lifecycle management. + +``` +┌──────────────┐ stdin/stdout ┌──────────────┐ +│ Codex │ ◄── JSON-RPC ──► │ Plugin │ +│ Server │ │ (Node.js) │ +└──────────────┘ └──────────────┘ +``` + +### Plugin Types + +| Type | Capability | Description | +|------|-----------|-------------| +| **Metadata** | `metadataProvider: ["series"]` or `["book"]` | Fetch series/book metadata from external sources | +| **Sync** | `userReadSync: true` | Bidirectional reading progress sync with external trackers | +| **Recommendation** | `userRecommendationProvider: true` | Generate personalized series recommendations | + +### Lifecycle + +1. **Spawn** — Codex launches the plugin process +2. **Initialize** — Codex sends config, credentials, and a storage handle +3. **Requests** — Codex sends capability-specific requests (search, sync, etc.) +4. **Ping** — periodic health checks +5. **Shutdown** — graceful termination + +## Build Your First Plugin: A Metadata Provider + +Let's build a simple metadata plugin that searches a fictional API for series information. + +### 1. Project Setup + +Create a new directory and initialize the project: ```bash -mkdir codex-plugin-metadata-myplugin -cd codex-plugin-metadata-myplugin +mkdir codex-plugin-metadata-example +cd codex-plugin-metadata-example npm init -y ``` -### 2. Install Dependencies +Install the SDK and development tools: ```bash npm install @ashdev/codex-plugin-sdk -npm install -D typescript @types/node esbuild +npm install -D typescript esbuild @types/node vitest @biomejs/biome ``` -### 3. Configure TypeScript +### 2. Configure TypeScript Create `tsconfig.json`: @@ -35,431 +66,885 @@ Create `tsconfig.json`: "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "declaration": true, + "sourceMap": true, "strict": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] } ``` -Update `package.json`: +Update `package.json` with build scripts and ES module settings: ```json { - "name": "@ashdev/codex-plugin-metadata-myplugin", + "name": "@yourname/codex-plugin-metadata-example", + "version": "1.0.0", "type": "module", "main": "dist/index.js", + "bin": "dist/index.js", + "files": ["dist"], + "engines": { "node": ">=22.0.0" }, "scripts": { - "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --sourcemap", - "start": "node dist/index.js", - "typecheck": "tsc --noEmit" + "build": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'", + "dev": "npm run build -- --watch", + "test": "vitest run", + "start": "node dist/index.js" } } ``` -### 4. Write Your Plugin +Key points: +- **`"type": "module"`** — plugins use ES modules +- **`"bin"`** — makes the plugin executable via `npx` +- **esbuild** bundles everything into a single file with a Node.js shebang + +### 3. Define the Manifest + +The manifest tells Codex what your plugin can do. Create `src/manifest.ts`: + +```typescript +import type { PluginManifest } from "@ashdev/codex-plugin-sdk"; +import packageJson from "../package.json" with { type: "json" }; + +export const manifest = { + name: "metadata-example", + displayName: "Example Metadata Plugin", + version: packageJson.version, + description: "Fetches series metadata from Example API", + author: "Your Name", + homepage: "https://github.com/your/repo", + protocolVersion: "1.0", + + capabilities: { + metadataProvider: ["series"], // "series", "book", or both + }, + + // Admin-configurable settings (Settings > Plugins > Configuration) + configSchema: { + description: "Plugin settings", + fields: [ + { + key: "maxResults", + label: "Maximum Results", + description: "Max results per search (1-20)", + type: "number" as const, + required: false, + default: 5, + }, + ], + }, +} as const satisfies PluginManifest; +``` + +#### Manifest Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Lowercase, alphanumeric with hyphens. Must be unique. | +| `displayName` | Yes | User-facing name shown in the UI | +| `version` | Yes | Semver string | +| `description` | Yes | Short description | +| `protocolVersion` | Yes | Always `"1.0"` for current protocol | +| `capabilities` | Yes | What the plugin provides (see Plugin Types above) | +| `configSchema` | No | Admin-configurable settings | +| `userConfigSchema` | No | Per-user settings | +| `requiredCredentials` | No | API keys or tokens (encrypted at rest) | +| `oauth` | No | OAuth 2.0 configuration for external services | +| `adminSetupInstructions` | No | Shown to admins during plugin configuration | +| `userSetupInstructions` | No | Shown to users when connecting | + +### 4. Implement the Provider Create `src/index.ts`: ```typescript import { createMetadataPlugin, + createLogger, + type InitializeParams, type MetadataProvider, type MetadataSearchParams, type MetadataSearchResponse, type MetadataGetParams, type PluginSeriesMetadata, - type PluginManifest, - type MetadataContentType, + type MetadataMatchParams, + type MetadataMatchResponse, + NotFoundError, + RateLimitError, } from "@ashdev/codex-plugin-sdk"; +import { manifest } from "./manifest.js"; -// Define your plugin manifest -const manifest = { - name: "metadata-myplugin", - displayName: "My Metadata Plugin", - version: "1.0.0", - description: "A custom metadata provider", - author: "Your Name", - protocolVersion: "1.0", - capabilities: { - metadataProvider: ["series"] as MetadataContentType[], - }, - // Optional: credentials your plugin needs - requiredCredentials: [ - { - key: "api_key", - label: "API Key", - description: "Your API key for the metadata service", - required: true, - sensitive: true, - type: "password", - }, - ], -} as const satisfies PluginManifest & { capabilities: { metadataProvider: MetadataContentType[] } }; +// Logger writes to stderr (stdout is reserved for JSON-RPC) +const logger = createLogger({ name: "example", level: "debug" }); + +// Plugin state (populated during initialization) +let maxResults = 5; // Implement the MetadataProvider interface const provider: MetadataProvider = { async search(params: MetadataSearchParams): Promise { - // Implement your search logic - const results = await fetchResults(params.query); + logger.info(`Searching for: ${params.query}`); + + // Call your external API here + const results = await fetchFromApi(params.query); return { - results: results.map(r => ({ - externalId: r.id, - title: r.title, - alternateTitles: [], - year: r.year, - coverUrl: r.cover, - relevanceScore: 0.9, + results: results.slice(0, maxResults).map((item, i) => ({ + externalId: item.id, + title: item.title, + alternateTitles: item.altTitles || [], + year: item.year, + relevanceScore: Math.max(0.1, 1.0 - i * 0.1), preview: { - status: r.status, - genres: r.genres.slice(0, 3), - description: r.summary?.slice(0, 200), + status: item.status, + genres: item.genres, + description: item.description, }, })), }; }, async get(params: MetadataGetParams): Promise { - // Implement your get logic - const series = await fetchSeries(params.externalId); + logger.info(`Getting metadata for: ${params.externalId}`); + + const item = await fetchById(params.externalId); + if (!item) { + throw new NotFoundError(`Series not found: ${params.externalId}`); + } + + return { + externalId: item.id, + externalUrl: item.url, + title: item.title, + summary: item.description, + status: item.status, + year: item.year, + genres: item.genres, + tags: item.tags, + authors: item.authors, + coverUrl: item.coverUrl, + rating: item.rating + ? { score: item.rating, voteCount: item.votes, source: "example" } + : undefined, + }; + }, + + // Optional: auto-match by title (called during library scans) + async match(params: MetadataMatchParams): Promise { + logger.info(`Matching: ${params.title}`); + + const results = await fetchFromApi(params.title); + const best = results[0]; + + if (!best) { + return { match: null, confidence: 0, alternatives: [] }; + } return { - externalId: series.id, - externalUrl: `https://example.com/series/${series.id}`, - title: series.title, - alternateTitles: [ - { title: series.nativeTitle, language: "ja", titleType: "native" }, - ], - summary: series.description, - status: series.status, - year: series.year, - genres: series.genres, - tags: series.tags, - authors: series.authors, - artists: series.artists, - externalLinks: [], + match: { + externalId: best.id, + title: best.title, + alternateTitles: [], + year: best.year, + relevanceScore: 0.9, + }, + confidence: 0.85, + alternatives: results.slice(1, 4).map((r) => ({ + externalId: r.id, + title: r.title, + alternateTitles: [], + relevanceScore: 0.6, + })), }; }, }; // Start the plugin -createMetadataPlugin({ manifest, provider }); +createMetadataPlugin({ + manifest, + provider, + logLevel: "debug", + onInitialize(params: InitializeParams) { + // Read admin configuration + const configured = params.adminConfig?.maxResults as number | undefined; + if (configured !== undefined) { + maxResults = Math.min(Math.max(1, configured), 20); + } + logger.info(`Plugin initialized (maxResults: ${maxResults})`); + }, +}); + +logger.info("Example plugin started"); ``` -### 5. Build and Test +#### Error Handling + +The SDK provides error classes that automatically convert to proper JSON-RPC error responses: + +```typescript +import { + NotFoundError, // Resource not found (code: -32001) + RateLimitError, // Rate limited, includes retryAfterSeconds (code: -32003) + AuthError, // Authentication failed (code: -32002) + ApiError, // External API error (code: -32004) + ConfigError, // Configuration error (code: -32005) +} from "@ashdev/codex-plugin-sdk"; + +// In your provider methods: +throw new NotFoundError("Series not found"); +throw new RateLimitError(60, "Rate limited by external API"); +throw new AuthError("Invalid API key"); +``` + +### 5. Build and Test Locally + +Build the plugin: ```bash npm run build +``` -# Test manually -echo '{"jsonrpc":"2.0","id":1,"method":"initialize"}' | node dist/index.js +Test it by running directly — the plugin reads JSON-RPC from stdin: + +```bash +echo '{"jsonrpc":"2.0","method":"initialize","params":{"adminConfig":{},"userConfig":{},"credentials":{}},"id":1}' | node dist/index.js ``` -## Plugin Manifest +You should see a JSON-RPC response with the manifest on stdout, and log messages on stderr. -The manifest describes your plugin's capabilities and requirements: +### 6. Install in Codex -```typescript -interface PluginManifest { - // Required - name: string; // Unique identifier (lowercase, alphanumeric, hyphens) - displayName: string; // Human-readable name - version: string; // Semver version - description: string; // Short description - author: string; // Author name - protocolVersion: "1.0"; // Protocol version - - // Capabilities - capabilities: { - metadataProvider?: MetadataContentType[]; // Content types: ["series"] or ["series", "book"] - syncProvider?: boolean; // Can sync reading progress (future) - recommendationProvider?: boolean; // Can provide recommendations (future) - }; +Three ways to install your plugin: - // Optional - homepage?: string; // Documentation URL - icon?: string; // Icon URL - requiredCredentials?: CredentialField[]; // API keys, etc. -} +#### Option A: Local Path (Development) + +In Codex **Settings > Plugins > Add Plugin**: +- **Command**: `node` +- **Arguments**: `/absolute/path/to/dist/index.js` + +#### Option B: npx (No Install Needed) + +Publish to npm, then configure: +- **Command**: `npx` +- **Arguments**: `-y @yourname/codex-plugin-metadata-example@1.0.0` + +#### Option C: Global Install + +```bash +npm install -g @yourname/codex-plugin-metadata-example ``` -## MetadataProvider Interface +Then configure: +- **Command**: `codex-plugin-metadata-example` (or whatever your `bin` name is) -Plugins must implement the `MetadataProvider` interface: +After adding the plugin, go to **Settings > Plugins**, review the requested permissions, and enable it. + +## Logging + +Plugins must **only write to stderr** for logging — stdout is reserved for JSON-RPC communication. The SDK logger handles this automatically: ```typescript -interface MetadataProvider { - search(params: MetadataSearchParams): Promise; - get(params: MetadataGetParams): Promise; - match?(params: MetadataMatchParams): Promise; -} +import { createLogger } from "@ashdev/codex-plugin-sdk"; + +const logger = createLogger({ name: "my-plugin", level: "debug" }); + +logger.debug("Detailed debug info", { query: "naruto" }); +logger.info("Operation completed"); +logger.warn("Something unexpected", { code: 429 }); +logger.error("Operation failed", { error: err.message }); ``` -The SDK automatically routes scoped method calls to your provider: -- `metadata/series/search` → `provider.search()` -- `metadata/series/get` → `provider.get()` -- `metadata/series/match` → `provider.match()` +Log levels: `debug`, `info`, `warn`, `error`. -### search +## Plugin Storage -Search for metadata by query string: +Plugins can persist data across restarts using the storage API. Storage is scoped per user-plugin connection — each user's data is isolated. ```typescript -async search(params: MetadataSearchParams): Promise { - // params.query - Search query string - // params.limit - Maximum results to return - // params.cursor - Pagination cursor from previous response +import { type PluginStorage } from "@ashdev/codex-plugin-sdk"; - return { - results: [ - { - externalId: "123", - title: "Series Title", - alternateTitles: ["Alt Title"], - year: 2024, - coverUrl: "https://example.com/cover.jpg", - relevanceScore: 0.95, // 0.0-1.0 - preview: { - status: "ongoing", - genres: ["Action", "Adventure"], - rating: 8.5, - description: "Brief description...", - }, - }, - ], - nextCursor: "page2", // Optional: for pagination - }; +// Storage is provided during initialization +let storage: PluginStorage; + +onInitialize(params) { + storage = params.storage; } + +// Basic operations +await storage.set("cache-key", { data: "value" }); +await storage.set("temp-key", { data: "value" }, "2025-12-31T00:00:00Z"); // With TTL +const result = await storage.get("cache-key"); // { data, expiresAt? } +await storage.delete("cache-key"); + +// List and clear +const keys = await storage.list(); // { keys: [{ key, expiresAt? }] } +await storage.clear(); // { deletedCount } ``` -### get +### Storage Limits + +- **100 keys** per user-plugin connection +- **1 MB** per value +- Limits enforced on writes only + +## Configuration Patterns -Get full metadata for an external ID: +Plugins receive configuration during initialization from three sources: + +### Admin Config (`configSchema`) + +Set by the Codex administrator in **Settings > Plugins > Configuration**. Use this for settings that apply to all users (e.g., result limits, API endpoints). ```typescript -async get(params: MetadataGetParams): Promise { - // params.externalId - ID from search results +configSchema: { + fields: [ + { + key: "maxResults", + label: "Maximum Results", + type: "number" as const, + required: false, + default: 10, + }, + ], +}, +``` - return { - externalId: "123", - externalUrl: "https://example.com/series/123", - title: "Series Title", - alternateTitles: [ - { title: "日本語タイトル", language: "ja", titleType: "native" }, - { title: "Romanized Title", language: "ja-Latn", titleType: "romaji" }, - ], - summary: "Full description...", - status: "ongoing", - year: 2024, - genres: ["Action", "Adventure"], - tags: ["Fantasy", "Magic"], - authors: ["Author Name"], - artists: ["Artist Name"], - publisher: "Publisher Name", - rating: { score: 85, voteCount: 1000, source: "example" }, - externalLinks: [ - { url: "https://example.com/123", label: "Example", linkType: "provider" }, - ], - }; -} +### User Config (`userConfigSchema`) + +Per-user settings configured in **Settings > Integrations > Plugin Settings**. Use this for personal preferences. + +```typescript +userConfigSchema: { + fields: [ + { + key: "progressUnit", + label: "Progress Unit", + type: "string" as const, + required: false, + default: "volumes", + }, + ], +}, ``` -### match (Optional) +### The `_codex` Namespace + +For sync plugins, the server stores generic sync settings under the `_codex` key in the user config. These are **server-interpreted** — the plugin never reads them. They control which entries the server sends: -Find best match for existing content (used for auto-matching): +| Key | Default | Description | +|-----|---------|-------------| +| `includeCompleted` | `true` | Include fully-read series | +| `includeInProgress` | `true` | Include partially-read series | +| `countPartialProgress` | `false` | Count partially-read books | +| `syncRatings` | `true` | Include scores and notes | + +### Credentials (`requiredCredentials`) + +API keys and tokens, encrypted at rest by Codex: ```typescript -async match(params: MetadataMatchParams): Promise { - // params.title - Title to match - // params.year - Year hint - // params.author - Author hint +requiredCredentials: [ + { + key: "api_key", + label: "API Key", + type: "password" as const, + required: true, + sensitive: true, + }, +], +``` - return { - match: bestResult, // Best match or null - confidence: 0.85, // 0.0-1.0 confidence score - alternatives: [...], // Other possible matches if confidence is low - }; +### Reading Config in `onInitialize` + +```typescript +onInitialize(params: InitializeParams) { + const adminMax = params.adminConfig?.maxResults as number | undefined; + const userUnit = params.userConfig?.progressUnit as string | undefined; + const apiKey = params.credentials?.api_key as string | undefined; + + // storage is also available here + storage = params.storage; } ``` -## Error Handling +## Building a Sync Plugin + +Sync plugins enable bidirectional reading progress synchronization with external tracking services (e.g., AniList, MyAnimeList). + +### Manifest + +```typescript +import { EXTERNAL_ID_SOURCE_ANILIST, type PluginManifest } from "@ashdev/codex-plugin-sdk"; + +export const manifest = { + name: "sync-example", + displayName: "Example Sync", + version: "1.0.0", + protocolVersion: "1.0", + description: "Sync reading progress with Example Tracker", + author: "Your Name", + + capabilities: { + userReadSync: true, + externalIdSource: "api:example", // Prefix for external ID matching + }, + + // OAuth for automatic authentication + oauth: { + authorizationUrl: "https://example.com/oauth/authorize", + tokenUrl: "https://example.com/oauth/token", + scopes: ["read", "write"], + pkce: true, // Recommended when supported + }, + + requiredCredentials: [ + { + key: "access_token", + label: "Access Token", + type: "password" as const, + required: true, + sensitive: true, + }, + ], + + userConfigSchema: { + description: "Sync settings", + fields: [ + { + key: "progressUnit", + label: "Progress Unit", + type: "string" as const, + required: false, + default: "volumes", + }, + ], + }, +} as const satisfies PluginManifest; +``` -Use SDK error classes for proper error reporting: +### SyncProvider Interface ```typescript import { - RateLimitError, - NotFoundError, + createSyncPlugin, + createLogger, + type SyncProvider, + type ExternalUserInfo, + type SyncPushRequest, + type SyncPushResponse, + type SyncPullRequest, + type SyncPullResponse, AuthError, - ApiError, - ConfigError, } from "@ashdev/codex-plugin-sdk"; +import { manifest } from "./manifest.js"; -// Rate limited by API -if (response.status === 429) { - const retryAfter = response.headers.get("Retry-After") || "60"; - throw new RateLimitError(parseInt(retryAfter, 10)); -} +const logger = createLogger({ name: "sync-example" }); +let accessToken: string; -// Resource not found -if (response.status === 404) { - throw new NotFoundError("Series not found"); -} +const provider: SyncProvider = { + // Return the authenticated user's profile + async getUserInfo(): Promise { + const user = await fetchUser(accessToken); + return { + externalId: user.id.toString(), + username: user.name, + avatarUrl: user.avatar, + profileUrl: user.url, + }; + }, -// Authentication failed -if (response.status === 401) { - throw new AuthError("Invalid API key"); -} + // Push local reading progress to the external service + async pushProgress(params: SyncPushRequest): Promise { + const successes: string[] = []; + const failures: Array<{ externalId: string; error: string }> = []; + + for (const entry of params.entries) { + try { + await updateExternalProgress(accessToken, { + externalId: entry.externalId, + status: entry.status, // reading, completed, on_hold, dropped, plan_to_read + progress: entry.progress, // { chapters?, volumes?, pages? } + rating: entry.rating, // 0-100 + startedAt: entry.startedAt, + completedAt: entry.completedAt, + }); + successes.push(entry.externalId); + } catch (err) { + failures.push({ externalId: entry.externalId, error: String(err) }); + } + } -// Generic API error -if (!response.ok) { - throw new ApiError(`API error: ${response.status}`, response.status); -} + return { successes, failures }; + }, -// Configuration error -if (!apiKey) { - throw new ConfigError("api_key credential is required"); -} + // Pull reading progress from the external service + async pullProgress(params: SyncPullRequest): Promise { + const list = await fetchReadingList(accessToken, { + page: params.page || 1, + updatedSince: params.updatedSince, + }); + + return { + entries: list.items.map((item) => ({ + externalId: item.id.toString(), + title: item.title, + status: mapStatus(item.status), + progress: { + chapters: item.chaptersRead, + volumes: item.volumesRead, + }, + rating: item.score, + startedAt: item.startDate, + lastReadAt: item.updatedAt, + completedAt: item.completionDate, + latestUpdatedAt: item.updatedAt, // Used for staleness detection + })), + hasMore: list.hasNextPage, + nextPage: list.hasNextPage ? (params.page || 1) + 1 : undefined, + }; + }, + + // Optional: return sync status summary + async status() { + return { lastSyncAt: new Date().toISOString() }; + }, +}; + +createSyncPlugin({ + manifest, + provider, + onInitialize(params) { + accessToken = params.credentials?.access_token as string; + if (!accessToken) throw new AuthError("No access token provided"); + }, +}); ``` -## Logging +### External ID Matching -Always log to stderr (stdout is reserved for JSON-RPC): +Sync plugins declare an `externalIdSource` in their manifest (e.g., `"api:example"`). Codex uses this to match series in your library with entries on the external service via the `series_external_ids` table. When pushing progress, Codex only sends entries that have a matching external ID. + +The SDK provides constants for known sources: ```typescript -import { createLogger } from "@ashdev/codex-plugin-sdk"; +import { EXTERNAL_ID_SOURCE_ANILIST } from "@ashdev/codex-plugin-sdk"; +// Value: "api:anilist" +``` -const logger = createLogger({ name: "metadata-myplugin", level: "info" }); +### OAuth Configuration -logger.debug("Processing request", { params }); -logger.info("Search completed", { resultCount: 10 }); -logger.warn("Rate limit approaching"); -logger.error("Request failed", error); +When `oauth` is defined in the manifest, Codex handles the full OAuth flow: -// NEVER use console.log() - it goes to stdout and breaks the protocol! -// Instead use: -console.error("Debug message"); // This is safe -``` +1. User clicks "Connect" in **Settings > Integrations** +2. Codex opens the authorization URL with CSRF state token and PKCE challenge +3. User authorizes on the external service +4. External service redirects to Codex's callback endpoint +5. Codex exchanges the code for tokens and stores them encrypted +6. Tokens are passed to the plugin as `credentials.access_token` -## Credential Delivery +The plugin never handles OAuth flows directly — it just receives the token. -Codex supports three methods for delivering credentials to plugins: +## Building a Recommendation Plugin -| Method | Value | Description | -|--------|-------|-------------| -| Environment Variables | `env` | Credentials passed as uppercase env vars (default) | -| Initialize Message | `init_message` | Credentials passed in the `initialize` JSON-RPC request | -| Both | `both` | Credentials passed both ways | +Recommendation plugins analyze the user's library and suggest new series. -### Using onInitialize Callback (Recommended) +### Manifest -Credentials are passed in the `initialize` request params: +```typescript +import type { PluginManifest } from "@ashdev/codex-plugin-sdk"; + +export const manifest = { + name: "recommendations-example", + displayName: "Example Recommendations", + version: "1.0.0", + protocolVersion: "1.0", + description: "Personalized recommendations from Example Service", + author: "Your Name", + + capabilities: { + userRecommendationProvider: true, + }, + + configSchema: { + description: "Recommendation settings", + fields: [ + { + key: "maxRecommendations", + label: "Maximum Recommendations", + type: "number" as const, + default: 20, + }, + { + key: "maxSeeds", + label: "Seed Titles", + description: "Number of top-rated library titles to use as input", + type: "number" as const, + default: 10, + }, + ], + }, + + // OAuth if the service requires authentication + oauth: { + authorizationUrl: "https://example.com/oauth/authorize", + tokenUrl: "https://example.com/oauth/token", + }, + + requiredCredentials: [ + { key: "access_token", label: "Access Token", type: "password" as const, required: true, sensitive: true }, + ], +} as const satisfies PluginManifest; +``` + +### RecommendationProvider Interface ```typescript -import { createMetadataPlugin, ConfigError, type InitializeParams } from "@ashdev/codex-plugin-sdk"; +import { + createRecommendationPlugin, + createLogger, + type RecommendationProvider, + type RecommendationRequest, + type RecommendationResponse, + type PluginStorage, +} from "@ashdev/codex-plugin-sdk"; +import { manifest } from "./manifest.js"; -let apiKey: string | undefined; +const logger = createLogger({ name: "recs-example" }); +let storage: PluginStorage; +let maxRecommendations = 20; +let maxSeeds = 10; -createMetadataPlugin({ +const provider: RecommendationProvider = { + // Generate recommendations based on user's library + async get(params: RecommendationRequest): Promise { + // params.library contains the user's series with ratings, genres, tags + const seeds = params.library + .sort((a, b) => (b.userRating || 0) - (a.userRating || 0)) + .slice(0, maxSeeds); + + logger.info(`Generating recommendations from ${seeds.length} seeds`); + + // Fetch recommendations from external API based on seeds + const recs = await fetchRecommendations(seeds); + + // Exclude series already in the library + const libraryIds = new Set( + params.library.flatMap((e) => e.externalIds?.map((id) => id.externalId) || []) + ); + // Also exclude explicitly dismissed series + const excludeIds = new Set(params.excludeIds || []); + + const filtered = recs + .filter((r) => !libraryIds.has(r.externalId) && !excludeIds.has(r.externalId)) + .slice(0, maxRecommendations); + + return { + recommendations: filtered.map((r) => ({ + externalId: r.externalId, + url: r.url, + title: r.title, + coverUrl: r.coverUrl, + description: r.description, + genres: r.genres, + rating: r.rating, + why: `Recommended because you liked "${r.basedOn}"`, + })), + }; + }, + + // Optional: dismiss a recommendation + async dismiss(params) { + // Store dismissed IDs to exclude from future results + const dismissed = ((await storage.get("dismissed"))?.data as string[]) || []; + dismissed.push(params.externalId); + await storage.set("dismissed", dismissed); + return { success: true }; + }, + + // Optional: clear cached data + async clear() { + await storage.clear(); + return { success: true }; + }, +}; + +createRecommendationPlugin({ manifest, provider, - onInitialize(params: InitializeParams) { - apiKey = params.credentials?.api_key; - if (!apiKey) { - throw new ConfigError("api_key credential is required"); - } + onInitialize(params) { + storage = params.storage; + maxRecommendations = (params.adminConfig?.maxRecommendations as number) || 20; + maxSeeds = (params.adminConfig?.maxSeeds as number) || 10; }, }); ``` -### Using Environment Variables +### Scoring Tips + +When scoring recommendations, consider: + +- **Community rating** from the external API (e.g., AniList `averageScore / 10`) +- **Relevance** to seed titles (genre overlap, tag similarity) +- **Duplicate boost** — if the same title appears from multiple seeds, boost its score (e.g., +0.05 per duplicate) +- **Score clamping** — keep final scores in the 0.0-1.0 range -Credentials are passed as environment variables (credential key in uppercase): +## Testing Your Plugin + +### Unit Tests with Vitest ```typescript -// Credential key "api_key" becomes environment variable "API_KEY" -const apiKey = process.env.API_KEY; +// src/manifest.test.ts +import { describe, it, expect } from "vitest"; +import { manifest } from "./manifest.js"; -if (!apiKey) { - throw new ConfigError("API_KEY environment variable is required"); -} +describe("manifest", () => { + it("has required fields", () => { + expect(manifest.name).toBe("metadata-example"); + expect(manifest.protocolVersion).toBe("1.0"); + expect(manifest.capabilities.metadataProvider).toContain("series"); + }); +}); ``` -## Testing Your Plugin +```typescript +// src/index.test.ts +import { describe, it, expect, vi } from "vitest"; + +describe("search", () => { + it("returns results for a query", async () => { + // Test your provider logic directly + const results = generateResults("naruto"); + expect(results).toHaveLength(5); + expect(results[0].title).toContain("naruto"); + }); +}); +``` -### Manual Testing +### Running Tests ```bash -# Initialize -echo '{"jsonrpc":"2.0","id":1,"method":"initialize"}' | node dist/index.js +# Run all tests +npx vitest run + +# Watch mode during development +npx vitest + +# With coverage +npx vitest run --coverage +``` + +### Manual Testing + +You can test the JSON-RPC protocol directly: -# Search (note the scoped method name) -echo '{"jsonrpc":"2.0","id":2,"method":"metadata/series/search","params":{"query":"test"}}' | node dist/index.js +```bash +# Build first +npm run build -# Ping -echo '{"jsonrpc":"2.0","id":3,"method":"ping"}' | node dist/index.js +# Send initialize + search requests +echo '{"jsonrpc":"2.0","method":"initialize","params":{"adminConfig":{},"userConfig":{},"credentials":{}},"id":1} +{"jsonrpc":"2.0","method":"metadata/series/search","params":{"query":"test"},"id":2}' | node dist/index.js ``` -### Unit Tests +## Common Patterns + +### Rate Limiting + +When calling external APIs, handle rate limits gracefully: ```typescript -import { describe, it, expect } from "vitest"; -import { mapSearchResult } from "./mappers"; +import { RateLimitError, ApiError } from "@ashdev/codex-plugin-sdk"; -describe("mappers", () => { - it("should map API response to SearchResult", () => { - const apiResponse = { id: "123", name: "Test" }; - const result = mapSearchResult(apiResponse); +async function callApi(url: string) { + const response = await fetch(url); - expect(result.externalId).toBe("123"); - expect(result.title).toBe("Test"); - }); -}); + if (response.status === 429) { + const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10); + throw new RateLimitError(retryAfter, "API rate limit exceeded"); + } + + if (!response.ok) { + throw new ApiError(`API error: ${response.status}`, response.status); + } + + return response.json(); +} ``` -## Deploying Your Plugin +### Pagination -### Local Installation +For pull operations that may return large datasets: + +```typescript +async pullProgress(params: SyncPullRequest): Promise { + const page = params.page || 1; + const data = await fetchPage(page); -1. Build your plugin: `npm run build` -2. In Codex admin UI, add a new plugin: - - Command: `node` - - Args: `/path/to/plugin/dist/index.js` - - Configure credentials + return { + entries: data.items, + hasMore: data.hasNextPage, + nextPage: data.hasNextPage ? page + 1 : undefined, + }; +} +``` -### Docker +Codex will keep calling `pullProgress` with incrementing pages until `hasMore` is `false`. -If running Codex in Docker, mount the plugins directory: +### Caching with Storage TTL -```yaml -volumes: - - ./my-plugin:/opt/codex/plugins/my-plugin:ro +```typescript +const CACHE_KEY = "api-cache"; +const CACHE_TTL_HOURS = 24; + +async function getCachedOrFetch(key: string): Promise { + const cached = await storage.get(key); + if (cached?.data) return cached.data; + + const fresh = await fetchFromApi(key); + const expiresAt = new Date(Date.now() + CACHE_TTL_HOURS * 3600_000).toISOString(); + await storage.set(key, fresh, expiresAt); + return fresh; +} ``` -Then configure: -- Command: `node` -- Args: `/opt/codex/plugins/my-plugin/dist/index.js` +## Reference Implementations + +The Codex repository includes three reference plugins: + +| Plugin | Location | Type | Description | +|--------|----------|------|-------------| +| **Echo** | `plugins/metadata-echo/` | Metadata | Minimal test plugin; echoes back queries as results. Great starting point. | +| **AniList Sync** | `plugins/sync-anilist/` | Sync | Full bidirectional sync with AniList. Shows OAuth, GraphQL, conflict resolution, staleness detection. | +| **AniList Recommendations** | `plugins/recommendations-anilist/` | Recommendation | Personalized recommendations from AniList. Shows scoring, deduplication, external ID resolution. | + +## Security Notes -## Best Practices +- **stdout** is reserved for JSON-RPC — never `console.log()` in production code; use the SDK logger (writes to stderr) +- **Credentials** (API keys, tokens) are encrypted at rest by Codex; treat them as sensitive +- **Storage** is scoped per user — one user cannot access another's plugin data +- Plugins run in a **sandboxed child process** with restricted environment variables +- All JSON-RPC requests have a **30-second timeout** -1. **Handle Rate Limits**: Respect API rate limits, throw `RateLimitError` with retry time -2. **Cache Responses**: Consider caching API responses to reduce load -3. **Normalize Data**: Map external data to standard Codex formats -4. **Graceful Degradation**: Return partial data rather than failing completely -5. **Log Appropriately**: Use debug level for request details, info for summary -6. **Test Thoroughly**: Write unit tests for mappers, integration tests for API client +## Protocol Versioning -## Example Plugins +Plugins declare `protocolVersion: "1.0"` in their manifest. The versioning contract: -- **Echo Plugin**: Simple test plugin - `plugins/metadata-echo/` -- **MangaBaka Plugin**: Full metadata provider - `plugins/metadata-mangabaka/` +- **Additive changes** (new optional fields, new methods) do NOT bump the version +- **Breaking changes** (removed fields, changed semantics) bump the major version +- Plugins should **ignore unknown fields** — this ensures forward compatibility +- Plugins built for `1.x` continue working as long as Codex supports major version `1` ## Next Steps -- [Plugin Protocol](./protocol.md) - Detailed protocol specification -- [Plugin SDK](./sdk.md) - Full SDK API documentation +- [Plugin Protocol](./protocol.md) — Detailed protocol specification +- [Plugin SDK](./sdk.md) — Full SDK API documentation diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index 9703f299..9a0d030d 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -537,6 +537,75 @@ CODEX_RATE_LIMIT_ENABLED=false Disabling rate limiting may expose your server to abuse. Only disable for trusted networks or development environments. ::: +## Plugin Credential Encryption + +Codex encrypts sensitive plugin data at rest — including OAuth tokens, refresh tokens, and plugin credentials — using **AES-256-GCM** authenticated encryption. This section covers setting up and managing the encryption key. + +### Setting Up the Encryption Key + +The encryption key is provided via the `CODEX_ENCRYPTION_KEY` environment variable. It must be a **base64-encoded 32-byte (256-bit) key**. + +Generate a key using OpenSSL: + +```bash +openssl rand -base64 32 +``` + +Then set it as an environment variable: + +```bash +export CODEX_ENCRYPTION_KEY="your-generated-base64-key-here" +``` + +Or in a Docker Compose file: + +```yaml +environment: + CODEX_ENCRYPTION_KEY: "your-generated-base64-key-here" +``` + +:::danger Required for Plugins +The encryption key is **required** when using sync or recommendation plugins that store OAuth tokens. Without it, plugin connection attempts will fail with a "Service Unavailable" error. Metadata-only plugins (like Open Library) do not require an encryption key. +::: + +### What the Key Protects + +| Data | When Encrypted | +|------|----------------| +| OAuth access tokens | When a user connects a sync/recommendation plugin | +| OAuth refresh tokens | When the external service issues a refresh token | +| Plugin credentials | When a plugin stores API keys or secrets | + +All encrypted values use a random 96-bit nonce, so encrypting the same token twice produces different ciphertext. Decryption requires the exact same key that was used for encryption. + +### Key Requirements + +- **Length**: Exactly 32 bytes (256 bits) before base64 encoding +- **Encoding**: Standard base64 (RFC 4648) +- **Persistence**: Must remain the same across Codex restarts — changing the key without re-encrypting data will make existing tokens undecryptable + +### Key Rotation + +Codex does not currently support automatic key rotation. If you need to rotate the encryption key, follow this manual procedure: + +1. **Stop Codex** — ensure no requests are in flight +2. **Have all users disconnect their plugins** — go to **Settings > Integrations** and click **Disconnect** on each plugin connection. This deletes the encrypted tokens from the database +3. **Update the encryption key** — set `CODEX_ENCRYPTION_KEY` to the new key +4. **Start Codex** +5. **Have users reconnect their plugins** — each user re-authorizes via OAuth, and new tokens are encrypted with the new key + +:::tip Simpler Alternative +Since disconnecting and reconnecting plugins re-issues fresh OAuth tokens encrypted with the current key, this is the simplest and safest rotation method. No data migration or scripting is required. +::: + +:::caution Lost Key +If you lose the encryption key, all stored OAuth tokens become undecryptable. Users will need to disconnect and reconnect their plugins to issue new tokens. No plugin configuration or storage data is lost — only the encrypted credentials. +::: + +### Future Enhancement + +Automatic key rotation with key versioning (storing the key version alongside encrypted data for seamless re-encryption) is planned for a future release. + ## Environment Variables All configuration options can be overridden with environment variables using the `CODEX_` prefix. @@ -602,6 +671,9 @@ CODEX_PDF_CACHE_DIR=data/cache CODEX_KOMGA_API_ENABLED=true CODEX_KOMGA_API_PREFIX=komga +# Plugin Credential Encryption +CODEX_ENCRYPTION_KEY=your-base64-encoded-32-byte-key + # Rate Limiting CODEX_RATE_LIMIT_ENABLED=true CODEX_RATE_LIMIT_ANONYMOUS_RPS=10 @@ -638,6 +710,7 @@ These settings are read from the config file at startup: - Server host/port - PDF rendering settings (DPI, cache directory, PDFium library path) - Rate limiting settings +- Plugin encryption key (`CODEX_ENCRYPTION_KEY`) ## Example Configurations @@ -725,6 +798,7 @@ CODEX_DATABASE_POSTGRES_USERNAME= CODEX_DATABASE_POSTGRES_PASSWORD= CODEX_DATABASE_POSTGRES_DATABASE_NAME=codex CODEX_AUTH_JWT_SECRET= +CODEX_ENCRYPTION_KEY= ``` ## Configuration Validation @@ -816,10 +890,11 @@ For detailed configuration, see the [Preprocessing Rules Guide](./preprocessing- ## Security Best Practices 1. **Use strong JWT secrets** - Generate with `openssl rand -base64 32` -2. **Never commit secrets** - Use environment variables or secret managers -3. **Use SSL for PostgreSQL** - Set `ssl_mode: verify-full` in production -4. **Restrict bind address** - Use `127.0.0.1` unless needed externally -5. **Disable API docs in production** - Set `enable_api_docs: false` +2. **Set a plugin encryption key** - Required for sync/recommendation plugins; generate with `openssl rand -base64 32` +3. **Never commit secrets** - Use environment variables or secret managers +4. **Use SSL for PostgreSQL** - Set `ssl_mode: verify-full` in production +5. **Restrict bind address** - Use `127.0.0.1` unless needed externally +6. **Disable API docs in production** - Set `enable_api_docs: false` ## Next Steps diff --git a/docs/docs/plugins/anilist-sync.md b/docs/docs/plugins/anilist-sync.md new file mode 100644 index 00000000..6b75d21b --- /dev/null +++ b/docs/docs/plugins/anilist-sync.md @@ -0,0 +1,171 @@ +--- +--- + +# AniList Sync Plugin + +The AniList sync plugin synchronizes manga reading progress between Codex and [AniList](https://anilist.co). It supports bidirectional sync of reading status, progress counts, scores, and dates. + +## Features + +- **Push** reading progress from Codex to AniList +- **Pull** reading progress from AniList to Codex +- Configurable sync direction (pull only, push only, or both) +- External ID matching via AniList media IDs (`api:anilist`) +- Highest-progress-wins conflict resolution + +## Setup + +### For Users + +**With OAuth (if configured by admin):** + +1. Go to **Settings** > **Integrations** +2. Click **Connect with AniList Sync** +3. Authorize Codex on AniList +4. You're connected! + +**With Personal Access Token:** + +1. Go to [AniList Developer Settings](https://anilist.co/settings/developer) +2. Create a new client with redirect URL `https://anilist.co/api/v2/oauth/pin` +3. Authorize your client and copy the token +4. In Codex, go to **Settings** > **Integrations** and paste the token + +### For Admins + +1. Go to **Settings** > **Plugins** > **Add Plugin** +2. Set command to `npx` with arguments `-y @ashdev/codex-plugin-sync-anilist` +3. Optionally configure OAuth (see the [plugin README](https://github.com/your-repo/plugins/sync-anilist) for details) + +## How Sync Works + +### Sync Modes + +Configure the sync direction in **Settings** > **Integrations** > **Settings**: + +| Mode | Description | +|------|-------------| +| **Pull & Push** (default) | Import from AniList, then export to AniList | +| **Pull Only** | Import from AniList without writing back | +| **Push Only** | Export to AniList without importing | + +### Sync Flow + +When a sync runs in **Pull & Push** mode, it executes two phases in order: + +``` +┌─────────────────────────────────────────────────────┐ +│ Sync Cycle │ +├──────────────────────┬──────────────────────────────┤ +│ Phase 1: Pull │ Phase 2: Push │ +│ │ │ +│ 1. Fetch AniList │ 1. Read Codex progress │ +│ reading list │ 2. Match series with │ +│ 2. Match entries to │ external IDs │ +│ Codex series via │ 3. Build push entries │ +│ external IDs │ 4. Send to AniList │ +│ 3. Mark matched │ (overwrites remote) │ +│ books as read │ │ +│ (additive only) │ │ +└──────────────────────┴──────────────────────────────┘ +``` + +### External ID Matching + +The plugin matches Codex series to AniList entries using external IDs stored in the `series_external_ids` table with source `api:anilist`. These IDs correspond to AniList media IDs. + +Series without an AniList external ID are skipped during both pull and push. + +## Conflict Resolution + +Codex uses a **highest progress wins** strategy. There is no manual conflict resolution — progress can only move forward. + +### How Conflicts Are Resolved + +| Scenario | What happens | +|----------|-------------| +| **AniList ahead** — e.g., 5 vols read on AniList, 3 in Codex | Pull marks books 4–5 as read in Codex. Push sends 5 to AniList. Both agree. | +| **Codex ahead** — e.g., 5 vols read in Codex, 3 on AniList | Pull tries to mark first 3 as read — already completed, skipped. Push sends 5 to AniList. **Codex wins.** | +| **Both changed** — different series or different progress | Pull applies remote progress (additive), then Push sends local state. **Codex wins** because push runs last. | + +### Key Behaviors + +- **Pull is additive only** — it marks unread books as read, but never un-reads a book. If you lower your progress on AniList, that change is ignored in Codex. +- **Push overwrites the remote** — after pulling, Codex sends its current state to AniList. This may overwrite changes made directly on AniList. +- **Progress is monotonic** — once a book is marked as read (either locally or via pull), sync will not undo it. Progress only moves forward. + +:::tip +If you want to manually manage your AniList list without Codex overwriting it, use **Pull Only** mode. If you only want to track progress from Codex to AniList without importing, use **Push Only** mode. +::: + +## Completed Status + +The plugin is conservative about marking series as "Completed" on AniList: + +- A series is pushed as **Completed** only when **all** local books are read **and** the series metadata includes a `total_book_count` that matches. +- Otherwise, the series is always pushed as **Reading** — even if all local books are read — because Codex can't be certain the library contains the full series. + +This prevents incorrectly marking a series as finished when you may simply not have all volumes in your library yet. + +## Sync Settings + +Sync settings are split into two categories: **Codex Sync Settings** (shared across all sync plugins) and **Plugin-Specific Settings** (AniList-only). + +### Codex Sync Settings + +These settings control which entries Codex sends to the plugin. They apply to all sync plugins, not just AniList. Configure them in **Settings** > **Integrations** > **Sync Settings**: + +| Option | Default | Description | +|--------|---------|-------------| +| **Include Completed Series** | On | Include series where all local books are marked as read | +| **Include In-Progress Series** | On | Include series where at least one book has been started | +| **Count Partially-Read Books** | Off | Whether partially-read books (started but not finished) count toward the progress number | +| **Sync Ratings** | On | Include scores and notes in push/pull operations | + +These settings are stored in the user plugin config under the `_codex` namespace (e.g., `_codex.includeCompleted`). The server reads them to filter which entries to build and send — this is the server's only role. The plugin never reads these settings. + +### Plugin-Specific Settings + +These settings are specific to the AniList plugin and control how it interprets the data from Codex. Configure them in **Settings** > **Integrations** > **Plugin Settings**: + +| Option | Default | Description | +|--------|---------|-------------| +| **Progress Unit** | Volumes | Whether each Codex book counts as a "volume" or "chapter" on AniList | +| **Pause After Days** | Disabled | Mark series as "Paused" on AniList if no reading progress for this many days | +| **Drop After Days** | Disabled | Mark series as "Dropped" on AniList if no reading progress for this many days | + +### Progress Unit: Volumes vs Chapters + +AniList tracks both volume and chapter progress separately. Codex always sends books-read as `volumes` in the sync data. The plugin then maps this to the correct AniList field based on your `progressUnit` setting: + +- **Volumes** (default) — sends progress as `progressVolumes`, which is the natural mapping for manga volumes. AniList displays this as "Read Vol. X". +- **Chapters** — sends progress as `progress` (chapter count). Only use this if your Codex books represent individual chapters rather than collected volumes. AniList displays this as "Read Ch. X". + +:::warning +Using "chapters" when your books are volumes can create misleading activity on your AniList profile (e.g., showing "Read chapter 3" when you actually read volume 3, which may contain chapters 20–30). +::: + +### Staleness Detection + +When `pauseAfterDays` or `dropAfterDays` is configured, the plugin checks each entry's `latestUpdatedAt` timestamp (the most recent reading progress update in Codex). If the elapsed time exceeds the threshold, the plugin overrides the entry's status before pushing to AniList: + +- **Drop takes priority** — if both thresholds are met, the entry is marked as "Dropped" +- Only applies during push — staleness is not checked during pull + +## Sync Results + +After each sync, you can see a summary in **Settings** > **Integrations**: + +| Field | Meaning | +|-------|---------| +| **Pulled** | Entries fetched from AniList | +| **Matched** | Pulled entries that matched a Codex series via external ID | +| **Applied** | Books newly marked as read from pulled data | +| **Pushed** | Entries sent to AniList | +| **Push Failures** | Entries that failed to push (e.g., invalid media ID) | + +## Next Steps + +- [Plugins Overview](./index) — Managing plugins in Codex +- [Book Metadata](../book-metadata) — Book types and metadata fields +- [Libraries](../libraries) — Library setup and scanning diff --git a/docs/docs/plugins/index.md b/docs/docs/plugins/index.md index 18ee8c2f..b717f5b2 100644 --- a/docs/docs/plugins/index.md +++ b/docs/docs/plugins/index.md @@ -7,11 +7,19 @@ Codex supports metadata plugins that can automatically fetch and enrich your lib ## Available Plugins +### Metadata Plugins + | Plugin | Description | Source | |--------|-------------|--------| | [Open Library](./open-library) | Fetch book metadata from Open Library using ISBN or title search | Free, no API key required | | Echo (built-in) | Development/testing plugin that echoes back sample metadata | Included with Codex | +### Sync Plugins + +| Plugin | Description | Source | +|--------|-------------|--------| +| [AniList Sync](./anilist-sync) | Sync manga reading progress between Codex and AniList | Free, requires AniList account | + ## Plugin Capabilities Plugins can provide metadata for: @@ -35,8 +43,161 @@ Each plugin requests specific permissions for the metadata fields it can write. If you've manually edited a metadata field, you can lock it to prevent plugins from overwriting your changes. Toggle the lock icon next to any field in the metadata editor. +## Codex Sync Settings + +When using sync plugins (like AniList Sync), Codex provides a set of **generic sync settings** that apply to all sync plugins. These settings control which entries the server sends to the plugin. + +The settings are stored in the user's plugin config under the `_codex` namespace: + +```json +{ + "_codex": { + "includeCompleted": true, + "includeInProgress": true, + "countPartialProgress": false, + "syncRatings": true + } +} +``` + +| Key | Default | Description | +|-----|---------|-------------| +| `includeCompleted` | `true` | Include series where all local books are marked as read | +| `includeInProgress` | `true` | Include series where at least one book has been started | +| `countPartialProgress` | `false` | Count partially-read books in the progress count | +| `syncRatings` | `true` | Include scores and notes in push/pull operations | + +These are **server-interpreted** — the server reads them to filter and build sync entries. Plugins never read `_codex` keys. Plugin-specific settings (like `progressUnit` for AniList) live in the plugin's own `userConfigSchema` and are only read by the plugin. + +## Security Model + +Codex applies multiple layers of security to ensure plugins operate safely and user data is protected. + +### Credential & Token Encryption + +All sensitive data — OAuth tokens, API keys, and plugin credentials — is encrypted at rest using **AES-256-GCM** (authenticated encryption). Each value is encrypted with a random 96-bit nonce, ensuring identical plaintext produces different ciphertext. The encryption key is provided via the `CODEX_ENCRYPTION_KEY` environment variable (a base64-encoded 32-byte key). + +For key generation instructions, rotation procedures, and requirements, see the [Plugin Credential Encryption](../configuration#plugin-credential-encryption) section in the Configuration guide. + +### Data Isolation + +Plugin data is isolated per user. Each user-plugin connection has a unique `user_plugin_id`, and all storage operations — reads, writes, and deletes — are scoped to that ID at the database level. A plugin connected by one user cannot access another user's storage, tokens, or configuration. + +### Plugin Process Sandboxing + +Plugins run as **child processes** spawned by Codex, communicating over stdin/stdout via JSON-RPC. This provides process-level isolation: + +- **Command allowlist**: Only approved commands can be used to launch plugins (`node`, `npx`, `python`, `python3`, `uv`, `uvx`, and paths under `/opt/codex/plugins/`). Custom commands can be allowed via the `CODEX_PLUGIN_ALLOWED_COMMANDS` environment variable. +- **Environment variable blocklist**: Dangerous environment variables are stripped before spawning plugins, including `LD_*`, `DYLD_*`, `PATH`, `HOME`, `PYTHONPATH`, `NODE_PATH`, and others that could enable library injection or path manipulation. +- **Request timeout**: Every JSON-RPC request has a **30-second timeout**. If a plugin hangs or becomes unresponsive, the request fails gracefully rather than blocking the server. +- **Health monitoring**: Failed requests are tracked, and plugins that fail repeatedly are automatically disabled. + +### OAuth Security + +OAuth connections (used by sync and recommendation plugins) are protected by: + +- **CSRF state tokens**: Each OAuth flow generates a cryptographically random 32-byte state parameter. State tokens are single-use (consumed on callback) and expire after **5 minutes**. +- **PKCE (S256)**: When the external service supports it, Codex uses Proof Key for Code Exchange with SHA-256 challenge method to prevent authorization code interception. +- **Rate limiting**: Each user is limited to **3 concurrent pending OAuth flows**. Additional attempts return HTTP 429 until existing flows complete or expire. +- **Automatic cleanup**: Expired OAuth state entries are periodically removed from memory by a background cleanup task. + +### Storage Quotas + +Plugin storage is subject to per-plugin limits to prevent abuse: + +- **Maximum 100 keys** per user-plugin connection +- **Maximum 1 MB** per stored value + +These limits are enforced on writes only — existing data is not affected. Updating an existing key (upsert) does not count against the key limit. + +## Privacy & Data Handling + +### What Data Leaves Codex + +The data sent to external services depends on the plugin type: + +| Plugin Type | Data Sent | Destination | +|-------------|-----------|-------------| +| **Metadata** | Series titles, ISBNs, search queries | Metadata provider API (e.g., Open Library) | +| **Sync** | Series titles, reading progress (books read), scores, dates, reading status | Tracking service API (e.g., AniList) | +| **Recommendations** | Library series titles (used as "seed" entries) | Recommendation service API (e.g., AniList) | + +Codex never sends file contents, file paths, or raw images to external services. + +### What Data Is Stored Locally + +- **OAuth tokens**: Encrypted at rest in the Codex database (AES-256-GCM) +- **API keys / credentials**: Encrypted at rest in the Codex database +- **Plugin configuration**: Stored in the database, scoped per user-plugin connection +- **Plugin storage**: Key-value data stored by plugins (e.g., sync state, caches), scoped per user-plugin connection +- **Cached recommendations**: Stored locally in the database, refreshed on demand + +### Disconnecting a Plugin + +To remove all data associated with a plugin connection: + +1. Go to **Settings** > **Integrations** +2. Click **Disconnect** on the plugin +3. This deletes: OAuth tokens, stored credentials, plugin configuration, and all plugin storage data + +The external service retains any data already synced to it (e.g., your AniList reading list). To remove that data, use the external service's own settings. + +## Troubleshooting OAuth Connections + +### Popup Blocked + +**Symptom**: Clicking "Connect" opens nothing, or the browser blocks the popup. + +**Fix**: Allow popups for your Codex URL in your browser settings, then try again. + +### Redirect URI Mismatch + +**Symptom**: The external service shows "redirect_uri mismatch" or a similar error. + +**Fix**: Ensure the OAuth redirect URI configured in the external service matches your Codex URL exactly. For AniList, the redirect URL should be set in your [AniList Developer Settings](https://anilist.co/settings/developer). The correct redirect URL is shown in the plugin's OAuth configuration panel in **Settings** > **Plugins**. + +### Token Expired / "Not Connected" + +**Symptom**: A plugin that was previously connected now shows as disconnected or fails with authentication errors. + +**Fix**: OAuth tokens can expire. Click **Connect** again to re-authorize. Your plugin configuration and storage data are preserved — only the token is refreshed. + +### Rate Limited by External Service + +**Symptom**: Sync or recommendations fail with errors mentioning "rate limit", "429", or "too many requests". + +**Fix**: Wait a few minutes before retrying. AniList has a rate limit of approximately 90 requests per minute. If syncing a large library, the plugin automatically retries once on rate-limit responses. Repeated failures may require waiting longer. + +### "Connection Failed" or Timeout + +**Symptom**: OAuth flow completes but Codex shows "Connection failed", or the popup hangs. + +**Fix**: + +1. Check that your Codex server can reach the external service (network/firewall). +2. Ensure you completed the OAuth flow within 5 minutes — state tokens expire after that. +3. Try disconnecting and reconnecting the plugin. +4. Check the Codex server logs for detailed error messages. + +### Too Many Connection Attempts + +**Symptom**: Clicking "Connect" returns a "Too Many Requests" error. + +**Fix**: You have 3 or more pending OAuth flows. Wait for them to expire (5 minutes) or complete one of them, then try again. + ## Plugin Development Codex provides a TypeScript SDK for building metadata plugins. Plugins implement a JSON-RPC interface with methods for searching, matching, and retrieving metadata. See the Echo plugin (`plugins/metadata-echo/`) as a reference implementation. + +### Protocol Versioning + +Plugins declare their protocol version via `protocolVersion: "1.0"` in the manifest. The versioning contract: + +- **Additive changes** (new optional fields, new methods) do NOT bump the protocol version. Plugins should ignore unknown fields. +- **Breaking changes** (removed fields, changed semantics, required field changes) bump the major version. +- **No runtime negotiation** — the server checks the plugin's declared version and rejects incompatible plugins. +- **Old methods are preserved** within a major version. If a method signature changes in a backward-incompatible way, a new method name is used. + +This means plugins built for protocol `1.x` will continue to work as long as the server supports major version `1`. New optional fields (like `latestUpdatedAt` on `SyncEntry` or `totalVolumes` on `SyncProgress`) are additive and do not require a version bump. diff --git a/docs/package-lock.json b/docs/package-lock.json index f6d45f36..6169d3ec 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -29,86 +29,16 @@ "node": ">=20.0" } }, - "node_modules/@ai-sdk/gateway": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.24.tgz", - "integrity": "sha512-mflk80YF8hj8vrF9e1IHhovGKC1ubX+sY88pesSk3pUiXfH5VPO8dgzNnxjwsqsCZrnkHcztxS5cSl4TzSiEuA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.20", - "@vercel/oidc": "3.0.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/provider": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.1.tgz", - "integrity": "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/provider-utils": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.20.tgz", - "integrity": "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.1", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/react": { - "version": "2.0.119", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.119.tgz", - "integrity": "sha512-kl4CDAnKJ1z+Fc9cjwMQXLRqH5/gHhg8Jn9qW7sZ0LgL8VpiDmW+x+s8e588nE3eC88aL1OxOVyOE6lFYfWprw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider-utils": "3.0.20", - "ai": "5.0.117", - "swr": "^2.2.5", - "throttleit": "2.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1", - "zod": "^3.25.76 || ^4.1.8" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, "node_modules/@algolia/abtesting": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz", - "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.0.tgz", + "integrity": "sha512-cZfj+1Z1dgrk3YPtNQNt0H9Rr67P8b4M79JjUKGS0d7/EbFbGxGgSu6zby5f22KXo3LT0LZa4O2c6VVbupJuDg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" @@ -147,99 +77,99 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz", - "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==", + "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==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz", - "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.0.tgz", + "integrity": "sha512-v5bMZMEqW9U2l40/tTAaRyn4AKrYLio7KcRuHmLaJtxuJAhvZiE7Y62XIsF070juz4MN3eyvfQmI+y5+OVbZuA==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz", - "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.0.tgz", + "integrity": "sha512-7H3DgRyi7UByScc0wz7EMrhgNl7fKPDjKX9OcWixLwCj7yrRXDSIzwunykuYUUO7V7HD4s319e15FlJ9CQIIFQ==", "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz", - "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.0.tgz", + "integrity": "sha512-tXmkB6qrIGAXrtRYHQNpfW0ekru/qymV02bjT0w5QGaGw0W91yT+53WB6dTtRRsIrgS30Al6efBvyaEosjZ5uw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz", - "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.0.tgz", + "integrity": "sha512-4tXEsrdtcBZbDF73u14Kb3otN+xUdTVGop1tBjict+Rc/FhsJQVIwJIcTrOJqmvhtBfc56Bu65FiVOnpAZCxcw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz", - "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==", + "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==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", - "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.0.tgz", + "integrity": "sha512-RB9bKgYTVUiOcEb5bOcZ169jiiVW811dCsJoLT19DcbbFmU4QaK0ghSTssij35QBQ3SCOitXOUrHcGgNVwS7sQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" @@ -252,81 +182,81 @@ "license": "MIT" }, "node_modules/@algolia/ingestion": { - "version": "1.46.2", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz", - "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==", + "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==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.46.2", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz", - "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.0.tgz", + "integrity": "sha512-aSe6jKvWt+8VdjOaq2ERtsXp9+qMXNJ3mTyTc1VMhNfgPl7ArOhRMRSQ8QBnY8ZL4yV5Xpezb7lAg8pdGrrulg==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz", - "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.0.tgz", + "integrity": "sha512-p9tfI1bimAaZrdiVExL/dDyGUZ8gyiSHsktP1ZWGzt5hXpM3nhv4tSjyHtXjEKtA0UvsaHKwSfFE8aAAm1eIQA==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "@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" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz", - "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.0.tgz", + "integrity": "sha512-XshyfpsQB7BLnHseMinp3fVHOGlTv6uEHOzNK/3XrEF9mjxoZAcdVfY1OCXObfwRWX5qXZOq8FnrndFd44iVsQ==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2" + "@algolia/client-common": "5.48.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz", - "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.0.tgz", + "integrity": "sha512-Q4XNSVQU89bKNAPuvzSYqTH9AcbOOiIo6AeYMQTxgSJ2+uvT78CLPMG89RIIloYuAtSfE07s40OLV50++l1Bbw==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2" + "@algolia/client-common": "5.48.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz", - "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==", + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.0.tgz", + "integrity": "sha512-ZgxV2+5qt3NLeUYBTsi6PLyHcENQWC0iFppFZekHSEDA2wcLdTUjnaJzimTEULHIvJuLRCkUs4JABdhuJktEag==", "license": "MIT", "dependencies": { - "@algolia/client-common": "5.46.2" + "@algolia/client-common": "5.48.0" }, "engines": { "node": ">= 14.0.0" @@ -350,12 +280,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -364,29 +294,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "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", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -402,14 +332,23 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "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==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -431,12 +370,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "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==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -446,18 +385,27 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "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==", "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.27.1", + "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -467,6 +415,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "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", @@ -484,17 +441,26 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -523,27 +489,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -565,9 +531,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "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==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -591,14 +557,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -648,39 +614,39 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "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==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -753,13 +719,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -793,12 +759,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -808,12 +774,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -823,12 +789,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -838,12 +804,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -884,14 +850,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -901,13 +867,13 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { @@ -933,12 +899,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -948,13 +914,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -964,13 +930,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -980,17 +946,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "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==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1000,13 +966,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1032,13 +998,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "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==", "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.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1063,13 +1029,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "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==", "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.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1094,13 +1060,13 @@ } }, "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1110,12 +1076,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", - "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1173,12 +1139,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1203,12 +1169,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1249,13 +1215,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1265,15 +1231,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1299,13 +1265,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "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==", "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.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1330,12 +1296,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1345,12 +1311,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1360,16 +1326,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@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.4" + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1395,12 +1361,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1410,12 +1376,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1441,13 +1407,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1457,14 +1423,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1519,16 +1485,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" + "@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" }, "engines": { "node": ">=6.9.0" @@ -1569,12 +1535,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1584,13 +1550,13 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "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==", "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.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1615,13 +1581,13 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", - "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", + "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", @@ -1634,6 +1600,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "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", @@ -1650,12 +1625,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { @@ -1711,16 +1686,16 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", - "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.27.1", + "@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.27.1" + "@babel/plugin-syntax-typescript": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1745,13 +1720,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "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==", "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.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1777,13 +1752,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "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==", "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.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1793,80 +1768,80 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", - "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "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.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", + "@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.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^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.5", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-computed-properties": "^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.27.1", + "@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.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.0", - "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@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.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.5", + "@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.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@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.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.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@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.27.1", - "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@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.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@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.28.4", - "@babel/plugin-transform-regexp-modifiers": "^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.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.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.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1876,6 +1851,28 @@ "@babel/core": "^7.0.0-0" } }, + "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==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -1930,52 +1927,52 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", - "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "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.43.0" + "core-js-pure": "^3.48.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "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==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1983,9 +1980,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2216,6 +2213,41 @@ "postcss": "^8.4" } }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@csstools/postcss-color-function": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", @@ -2602,10 +2634,10 @@ "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", - "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", "funding": [ { "type": "github", @@ -2617,17 +2649,52 @@ } ], "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, "engines": { "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4" + "postcss-selector-parser": "^7.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, "node_modules/@csstools/postcss-logical-float-and-clear": { @@ -2829,9 +2896,9 @@ } }, "node_modules/@csstools/postcss-normalize-display-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", - "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz", + "integrity": "sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA==", "funding": [ { "type": "github", @@ -3036,6 +3103,19 @@ "postcss": "^8.4" } }, + "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==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@csstools/postcss-sign-functions": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", @@ -3216,50 +3296,6 @@ "postcss": "^8.4" } }, - "node_modules/@csstools/selector-resolve-nested": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", - "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, "node_modules/@csstools/utilities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", @@ -3292,9 +3328,9 @@ } }, "node_modules/@docsearch/core": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.4.0.tgz", - "integrity": "sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.5.4.tgz", + "integrity": "sha512-DbkfZbJyYAPFJtF71eAFOTQSy5z5c/hdSN0UrErORKDwXKLTJBR0c+5WxE5l+IKZx4xIaEa8RkrL7T28DTCOYw==", "license": "MIT", "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", @@ -3314,25 +3350,20 @@ } }, "node_modules/@docsearch/css": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.4.0.tgz", - "integrity": "sha512-e9vPgtih6fkawakmYo0Y6V4BKBmDV7Ykudn7ADWXUs5b6pmtBRwDbpSG/WiaUG63G28OkJDEnsMvgIAnZgGwYw==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.5.4.tgz", + "integrity": "sha512-gzO4DJwyM9c4YEPHwaLV1nUCDC2N6yoh0QJj44dce2rcfN71mB+jpu3+F+Y/KMDF1EKV0C3m54leSWsraE94xg==", "license": "MIT" }, "node_modules/@docsearch/react": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.4.0.tgz", - "integrity": "sha512-z12zeg1mV7WD4Ag4pKSuGukETJLaucVFwszDXL/qLaEgRqxEaVacO9SR1qqnCXvZztlvz2rt7cMqryi/7sKfjA==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.5.4.tgz", + "integrity": "sha512-iBNFfvWoUFRUJmGQ/r+0AEp2OJgJMoYIKRiRcTDON0hObBRSLlrv2ktb7w3nc1MeNm1JIpbPA99i59TiIR49fA==", "license": "MIT", "dependencies": { - "@ai-sdk/react": "^2.0.30", "@algolia/autocomplete-core": "1.19.2", - "@docsearch/core": "4.4.0", - "@docsearch/css": "4.4.0", - "ai": "^5.0.30", - "algoliasearch": "^5.28.0", - "marked": "^16.3.0", - "zod": "^4.1.8" + "@docsearch/core": "4.5.4", + "@docsearch/css": "4.5.4" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", @@ -3424,36 +3455,6 @@ } } }, - "node_modules/@docusaurus/bundler/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@docusaurus/bundler/node_modules/html-minifier-terser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", - "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "~5.3.2", - "commander": "^10.0.0", - "entities": "^4.4.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.15.1" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, "node_modules/@docusaurus/core": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", @@ -3515,32 +3516,6 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/core/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@docusaurus/core/node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@docusaurus/cssnano-preset": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", @@ -3661,46 +3636,6 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/@docusaurus/plugin-content-blog/node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "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", @@ -4070,6 +4005,20 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@docusaurus/utils": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", @@ -4145,9 +4094,9 @@ } }, "node_modules/@easyops-cn/docusaurus-search-local": { - "version": "0.52.2", - "resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.52.2.tgz", - "integrity": "sha512-oEHkHe/OWHFcJxOhicS5UqohDOyPieREH+oNoImL/VcdrPzDxT2LB5Scov6WMOpOyDcSMJ6QCvjj63PEhhU8Nw==", + "version": "0.52.3", + "resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.52.3.tgz", + "integrity": "sha512-bkKHD+FoAY+sBvd9vcHudx8X5JQXkyGBcpstpJwOUTTpKwT0rOtUtnfmizpMu113LqdHxOxvlekYkGeTNGYYvw==", "license": "MIT", "dependencies": { "@docusaurus/plugin-content-docs": "^2 || ^3", @@ -4177,6 +4126,43 @@ "react-dom": "^16.14.0 || 17 || ^18 || ^19" } }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/@easyops-cn/docusaurus-search-local/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -4191,20 +4177,70 @@ "node": ">=12" } }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", - "license": "MIT" - }, - "node_modules/@faker-js/faker": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", - "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", - "deprecated": "Please update to a newer version.", - "license": "MIT" + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } }, - "node_modules/@hapi/hoek": { + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" + }, + "node_modules/@faker-js/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", + "deprecated": "Please update to a newer version.", + "license": "MIT" + }, + "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", @@ -4337,9 +4373,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -4368,6 +4404,269 @@ "tslib": "2" } }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", + "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", + "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", + "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-print": "4.56.10", + "@jsonjoy.com/fs-snapshot": "4.56.10", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", + "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", + "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", + "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.56.10" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", + "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.56.10", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", + "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@jsonjoy.com/json-pack": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", @@ -4394,6 +4693,22 @@ "tslib": "2" } }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@jsonjoy.com/json-pointer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", @@ -4434,6 +4749,22 @@ "tslib": "2" } }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -4477,15 +4808,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@mdx-js/mdx/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -4503,6 +4825,30 @@ "react": ">=16" } }, + "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", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@node-rs/jieba": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/@node-rs/jieba/-/jieba-1.10.4.tgz", @@ -4532,6 +4878,38 @@ "@node-rs/jieba-win32-x64-msvc": "1.10.4" } }, + "node_modules/@node-rs/jieba-android-arm-eabi": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.10.4.tgz", + "integrity": "sha512-MhyvW5N3Fwcp385d0rxbCWH42kqDBatQTyP8XbnYbju2+0BO/eTeCCLYj7Agws4pwxn2LtdldXRSKavT7WdzNA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-android-arm64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm64/-/jieba-android-arm64-1.10.4.tgz", + "integrity": "sha512-XyDwq5+rQ+Tk55A+FGi6PtJbzf974oqnpyCcCPzwU3QVXJCa2Rr4Lci+fx8oOpU4plT3GuD+chXMYLsXipMgJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/jieba-darwin-arm64": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-arm64/-/jieba-darwin-arm64-1.10.4.tgz", @@ -4548,6 +4926,182 @@ "node": ">= 10" } }, + "node_modules/@node-rs/jieba-darwin-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-x64/-/jieba-darwin-x64-1.10.4.tgz", + "integrity": "sha512-MmDNeOb2TXIZCPyWCi2upQnZpPjAxw5ZGEj6R8kNsPXVFALHIKMa6ZZ15LCOkSTsKXVC17j2t4h+hSuyYb6qfQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-freebsd-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-freebsd-x64/-/jieba-freebsd-x64-1.10.4.tgz", + "integrity": "sha512-/x7aVQ8nqUWhpXU92RZqd333cq639i/olNpd9Z5hdlyyV5/B65LLy+Je2B2bfs62PVVm5QXRpeBcZqaHelp/bg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm-gnueabihf": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm-gnueabihf/-/jieba-linux-arm-gnueabihf-1.10.4.tgz", + "integrity": "sha512-crd2M35oJBRLkoESs0O6QO3BBbhpv+tqXuKsqhIG94B1d02RVxtRIvSDwO33QurxqSdvN9IeSnVpHbDGkuXm3g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-gnu/-/jieba-linux-arm64-gnu-1.10.4.tgz", + "integrity": "sha512-omIzNX1psUzPcsdnUhGU6oHeOaTCuCjUgOA/v/DGkvWC1jLcnfXe4vdYbtXMh4XOCuIgS1UCcvZEc8vQLXFbXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-musl/-/jieba-linux-arm64-musl-1.10.4.tgz", + "integrity": "sha512-Y/tiJ1+HeS5nnmLbZOE+66LbsPOHZ/PUckAYVeLlQfpygLEpLYdlh0aPpS5uiaWMjAXYZYdFkpZHhxDmSLpwpw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-x64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-gnu/-/jieba-linux-x64-gnu-1.10.4.tgz", + "integrity": "sha512-WZO8ykRJpWGE9MHuZpy1lu3nJluPoeB+fIJJn5CWZ9YTVhNDWoCF4i/7nxz1ntulINYGQ8VVuCU9LD86Mek97g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-x64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-musl/-/jieba-linux-x64-musl-1.10.4.tgz", + "integrity": "sha512-uBBD4S1rGKcgCyAk6VCKatEVQb6EDD5I40v/DxODi5CuZVCANi9m5oee/MQbAoaX7RydA2f0OSCE9/tcwXEwUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-wasm32-wasi": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-wasm32-wasi/-/jieba-wasm32-wasi-1.10.4.tgz", + "integrity": "sha512-Y2umiKHjuIJy0uulNDz9SDYHdfq5Hmy7jY5nORO99B4pySKkcrMjpeVrmWXJLIsEKLJwcCXHxz8tjwU5/uhz0A==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/jieba-win32-arm64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-arm64-msvc/-/jieba-win32-arm64-msvc-1.10.4.tgz", + "integrity": "sha512-nwMtViFm4hjqhz1it/juQnxpXgqlGltCuWJ02bw70YUDMDlbyTy3grCJPpQQpueeETcALUnTxda8pZuVrLRcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-win32-ia32-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-ia32-msvc/-/jieba-win32-ia32-msvc-1.10.4.tgz", + "integrity": "sha512-DCAvLx7Z+W4z5oKS+7vUowAJr0uw9JBw8x1Y23Xs/xMA4Em+OOSiaF5/tCJqZUCJ8uC4QeImmgDFiBqGNwxlyA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-win32-x64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-x64-msvc/-/jieba-win32-x64-msvc-1.10.4.tgz", + "integrity": "sha512-+sqemSfS1jjb+Tt7InNbNzrRh1Ua3vProVvC4BZRPg010/leCbGFFiQHpzcPRfpxAXZrzG5Y0YBTsPzN/I4yHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4583,27 +5137,18 @@ "node": ">= 8" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -4613,25 +5158,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -4649,9 +5194,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -4669,9 +5214,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -4689,9 +5234,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -4709,9 +5254,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -4729,9 +5274,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -4749,9 +5294,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -4769,9 +5314,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -4789,9 +5334,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -4809,9 +5354,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -4829,9 +5374,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -4849,9 +5394,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -4869,9 +5414,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -4888,57 +5433,218 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "optional": true, "engines": { - "node": ">=12.22.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", "license": "MIT", "dependencies": { - "graceful-fs": "4.2.10" - }, - "engines": { - "node": ">=12.22.0" + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "license": "ISC" + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } }, - "node_modules/@pnpm/npm-conf": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", "license": "MIT", "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, - "engines": { - "node": ">=12" + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, "node_modules/@redocly/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==", + "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==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -4978,31 +5684,30 @@ "npm": ">=9.5.0" } }, - "node_modules/@redocly/openapi-core/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==", + "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": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "license": "MIT" - }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "@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" }, - "engines": { - "node": ">=10" + "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/@sideway/address": { @@ -5027,9 +5732,9 @@ "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, "node_modules/@sindresorhus/is": { @@ -5055,84 +5760,18 @@ "micromark-util-symbol": "^1.0.1" } }, - "node_modules/@slorber/remark-comment/node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@slorber/remark-comment/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/@slorber/remark-comment/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/@slorber/remark-comment/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -5411,6 +6050,16 @@ "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", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -5449,18 +6098,6 @@ "@types/node": "*" } }, - "node_modules/@types/connect-history-api-fallback/node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5518,9 +6155,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -5550,18 +6187,6 @@ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", "license": "MIT" }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", - "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", - "license": "MIT", - "dependencies": { - "hoist-non-react-statics": "^3.3.0" - }, - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5569,9 +6194,9 @@ "license": "MIT" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "license": "MIT" }, "node_modules/@types/http-errors": { @@ -5647,41 +6272,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/parse5": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", - "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", - "license": "MIT" - }, "node_modules/@types/prismjs": { "version": "1.26.5", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -5695,26 +6299,14 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, - "node_modules/@types/react-redux": { - "version": "7.1.34", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", - "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", - "license": "MIT", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/react-router": { "version": "5.1.20", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", @@ -5780,39 +6372,6 @@ "@types/express": "*" } }, - "node_modules/@types/serve-index/node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/serve-index/node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/serve-index/node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, "node_modules/@types/serve-static": { "version": "1.15.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", @@ -5849,6 +6408,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -5879,15 +6444,6 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@vercel/oidc": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", - "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", - "license": "Apache-2.0", - "engines": { - "node": ">= 20" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6059,21 +6615,42 @@ "node": ">= 0.6" } }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { "acorn": "bin/acorn" }, "engines": { @@ -6144,24 +6721,6 @@ "node": ">=8" } }, - "node_modules/ai": { - "version": "5.0.117", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.117.tgz", - "integrity": "sha512-uE6HNkdSwxbeHGKP/YbvapwD8fMOpj87wyfT9Z00pbzOh2fpnw5acak/4kzU00SX2vtI9K0uuy+9Tf9ytw5RwA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/gateway": "2.0.24", - "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.20", - "@opentelemetry/api": "1.9.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -6222,25 +6781,25 @@ } }, "node_modules/algoliasearch": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz", - "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.12.2", - "@algolia/client-abtesting": "5.46.2", - "@algolia/client-analytics": "5.46.2", - "@algolia/client-common": "5.46.2", - "@algolia/client-insights": "5.46.2", - "@algolia/client-personalization": "5.46.2", - "@algolia/client-query-suggestions": "5.46.2", - "@algolia/client-search": "5.46.2", - "@algolia/ingestion": "1.46.2", - "@algolia/monitoring": "1.46.2", - "@algolia/recommend": "5.46.2", - "@algolia/requester-browser-xhr": "5.46.2", - "@algolia/requester-fetch": "5.46.2", - "@algolia/requester-node-http": "5.46.2" + "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" }, "engines": { "node": ">= 14.0.0" @@ -6259,9 +6818,9 @@ } }, "node_modules/allof-merge": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/allof-merge/-/allof-merge-0.6.7.tgz", - "integrity": "sha512-slvjkM56OdeVkm1tllrnaumtSHwqyHrepXkAe6Am+CW4WdbHkNqdOKPF6cvY3/IouzvXk1BoLICT5LY7sCoFGw==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/allof-merge/-/allof-merge-0.6.8.tgz", + "integrity": "sha512-RJrHVDqITsU1kjE2L7s1hy4AYZSTlO1m9jTleYhVCEOfOpbbygRGfcEgrp+bW3oX/PcMUwVkt6MSJyXoyI6lRA==", "license": "MIT", "dependencies": { "json-crawl": "^0.5.3" @@ -6405,6 +6964,20 @@ "node": ">=8" } }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -6415,24 +6988,15 @@ } }, "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "funding": [ { "type": "opencollective", @@ -6450,7 +7014,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -6492,19 +7056,28 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "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==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", @@ -6519,12 +7092,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "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==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.6" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6567,9 +7140,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "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==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -6626,6 +7199,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6635,6 +7217,18 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6680,13 +7274,12 @@ } }, "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -6780,14 +7373,23 @@ } }, "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -6815,18 +7417,6 @@ "node": ">=14.16" } }, - "node_modules/cacheable-request/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6924,9 +7514,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "funding": [ { "type": "opencollective", @@ -6969,18 +7559,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -7040,25 +7618,21 @@ } }, "node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", - "whatwg-mimetype": "^4.0.0" + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" }, "engines": { - "node": ">=20.18.1" + "node": ">= 6" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" @@ -7141,6 +7715,15 @@ "node": ">= 10.0" } }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7306,9 +7889,9 @@ "license": "MIT" }, "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "license": "MIT" }, "node_modules/combine-promises": { @@ -7363,6 +7946,15 @@ "node": ">= 0.6" } }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/compression": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", @@ -7381,6 +7973,15 @@ "node": ">= 0.8.0" } }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7433,6 +8034,12 @@ "proto-list": "~1.2.1" } }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/configstore": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", @@ -7589,9 +8196,9 @@ } }, "node_modules/core-js": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -7600,12 +8207,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "license": "MIT", "dependencies": { - "browserslist": "^4.28.0" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -7613,9 +8220,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", - "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", + "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": { @@ -7727,6 +8334,19 @@ "postcss": "^8.4" } }, + "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==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz", @@ -7766,6 +8386,41 @@ "postcss": "^8.4" } }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -7801,18 +8456,6 @@ } } }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/css-minimizer-webpack-plugin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", @@ -7857,21 +8500,6 @@ } } }, - "node_modules/css-minimizer-webpack-plugin/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/css-prefers-color-scheme": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", @@ -7936,9 +8564,9 @@ } }, "node_modules/cssdb": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.6.0.tgz", - "integrity": "sha512-7ZrRi/Z3cRL1d5I8RuXEWAkRFP3J4GeQRiyVknI4KC70RAU8hT4LysUZDe0y+fYNOktCbxE8sOPUOhyR12UqGQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.7.1.tgz", + "integrity": "sha512-+F6LKx48RrdGOtE4DT5jz7Uo+VeyKXpK797FAevIkzjV8bMHz6xTO5F7gNDcRCHmPgD5jj2g6QCsY9zmVrh38A==", "funding": [ { "type": "opencollective", @@ -8123,9 +8751,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -8150,6 +8778,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -8169,9 +8809,9 @@ } }, "node_modules/default-browser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", - "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -8277,16 +8917,13 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/detect-node": { @@ -8337,15 +8974,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -8371,26 +8999,26 @@ } }, "node_modules/docusaurus-plugin-openapi-docs": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.5.1.tgz", - "integrity": "sha512-3I6Sjz19D/eM86a24/nVkYfqNkl/zuXSP04XVo7qm/vlPeCpHVM4li2DLj7PzElr6dlS9RbaS4HVIQhEOPGBRQ==", + "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==", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.4", - "@redocly/openapi-core": "^1.10.5", + "@redocly/openapi-core": "^1.34.3", "allof-merge": "^0.6.6", "chalk": "^4.1.2", - "clsx": "^1.1.1", - "fs-extra": "^9.0.1", + "clsx": "^2.1.1", + "fs-extra": "^11.3.0", "json-pointer": "^0.6.2", "json5": "^2.2.3", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "mustache": "^4.2.0", - "openapi-to-postmanv2": "^4.21.0", - "postman-collection": "^4.4.0", - "slugify": "^1.6.5", + "openapi-to-postmanv2": "^5.0.0", + "postman-collection": "^5.0.2", + "slugify": "^1.6.6", "swagger2openapi": "^7.0.8", - "xml-formatter": "^2.6.1" + "xml-formatter": "^3.6.6" }, "engines": { "node": ">=14" @@ -8402,30 +9030,6 @@ "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/docusaurus-plugin-openapi-docs/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/docusaurus-plugin-openapi-docs/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/docusaurus-plugin-sass": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.6.tgz", @@ -8441,38 +9045,38 @@ } }, "node_modules/docusaurus-theme-openapi-docs": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.5.1.tgz", - "integrity": "sha512-C7mYh9JC3l9jjRtqJVu0EIyOgxHB08jE0Tp5NSkNkrrBak4A13SrXCisNjvt1eaNjS+tsz7qD0bT3aI5hsRvWA==", + "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==", "license": "MIT", "dependencies": { "@hookform/error-message": "^2.0.1", - "@reduxjs/toolkit": "^1.7.1", + "@reduxjs/toolkit": "^2.8.2", "allof-merge": "^0.6.6", "buffer": "^6.0.3", - "clsx": "^1.1.1", - "copy-text-to-clipboard": "^3.1.0", - "crypto-js": "^4.1.1", + "clsx": "^2.1.1", + "copy-text-to-clipboard": "^3.2.0", + "crypto-js": "^4.2.0", "file-saver": "^2.0.5", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "pako": "^2.1.0", - "postman-code-generators": "^1.10.1", - "postman-collection": "^4.4.0", - "prism-react-renderer": "^2.3.0", + "postman-code-generators": "^2.0.0", + "postman-collection": "^5.0.2", + "prism-react-renderer": "^2.4.1", "process": "^0.11.10", - "react-hook-form": "^7.43.8", - "react-live": "^4.0.0", + "react-hook-form": "^7.59.0", + "react-live": "^4.1.8", "react-magic-dropzone": "^1.0.1", - "react-markdown": "^8.0.1", - "react-modal": "^3.15.1", - "react-redux": "^7.2.0", - "rehype-raw": "^6.1.1", - "remark-gfm": "3.0.1", - "sass": "^1.80.4", - "sass-loader": "^16.0.2", + "react-markdown": "^10.1.0", + "react-modal": "^3.16.3", + "react-redux": "^9.2.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "4.0.1", + "sass": "^1.89.2", + "sass-loader": "^16.0.5", "unist-util-visit": "^5.0.0", - "url": "^0.11.1", - "xml-formatter": "^2.6.1" + "url": "^0.11.4", + "xml-formatter": "^3.6.6" }, "engines": { "node": ">=14" @@ -8485,2565 +9089,2387 @@ "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/@reduxjs/toolkit": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", - "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", - "license": "MIT", - "dependencies": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "license": "MIT", "dependencies": { - "@types/unist": "^2" + "utila": "~0.4" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { - "@types/unist": "^2" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, "engines": { - "node": ">=12" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-from-parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", - "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", - "license": "MIT", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "hastscript": "^7.0.0", - "property-information": "^6.0.0", - "vfile": "^5.0.0", - "vfile-location": "^4.0.0", - "web-namespaces": "^2.0.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-parse-selector": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", - "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-raw": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", - "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/parse5": "^6.0.0", - "hast-util-from-parse5": "^7.0.0", - "hast-util-to-parse5": "^7.0.0", - "html-void-elements": "^2.0.0", - "parse5": "^6.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-raw/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-to-parse5": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", - "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.4" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/hastscript": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", - "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "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==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^3.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 4" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/html-void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", - "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-find-and-replace": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", - "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=10.13.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.4" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", - "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", "license": "MIT", "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-gfm-autolink-literal": "^1.0.0", - "mdast-util-gfm-footnote": "^1.0.0", - "mdast-util-gfm-strikethrough": "^1.0.0", - "mdast-util-gfm-table": "^1.0.0", - "mdast-util-gfm-task-list-item": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-autolink-literal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", - "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-find-and-replace": "^2.0.0", - "micromark-util-character": "^1.0.0" + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-footnote": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", - "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-util-normalize-identifier": "^1.0.0" + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-strikethrough": { + "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", - "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "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", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" + "@types/estree": "^1.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", - "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.3.0" + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-task-list-item": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", - "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "@types/estree": "^1.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/remcohaszing" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0" + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "license": "MIT", "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "@types/estree": "^1.0.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", - "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^1.0.0", - "micromark-extension-gfm-footnote": "^1.0.0", - "micromark-extension-gfm-strikethrough": "^1.0.0", - "micromark-extension-gfm-table": "^1.0.0", - "micromark-extension-gfm-tagfilter": "^1.0.0", - "micromark-extension-gfm-task-list-item": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" + "engines": { + "node": ">=6.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/eta-dev/eta?sponsor=1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-autolink-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", - "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.6" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-footnote": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", - "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", - "license": "MIT", + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "@types/node": "*", + "require-like": ">= 0.1.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.8" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", - "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.8.x" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", - "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "license": "MIT", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-tagfilter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", - "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "license": "BSD-3-Clause" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { - "micromark-util-types": "^1.0.0" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://opencollective.com/express" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", - "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "safe-buffer": "5.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.6" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "ms": "2.0.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "funding": [ { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "type": "github", + "url": "https://github.com/sponsors/fastify" }, { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "type": "opencollective", + "url": "https://opencollective.com/fastify" } ], - "license": "MIT", + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "reusify": "^1.0.4" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", "license": "MIT", "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", "dependencies": { - "micromark-util-symbol": "^1.0.0" + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "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": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "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", - "dependencies": { - "micromark-util-symbol": "^1.0.0" + "engines": { + "node": ">=0.8.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", "license": "MIT", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/file-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==", "license": "MIT", "dependencies": { - "micromark-util-symbol": "^1.0.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", "license": "MIT" }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "engines": { + "node": ">=0.10.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/rehype-raw": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", - "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "hast-util-raw": "^7.2.0", - "unified": "^10.0.0" + "to-regex-range": "^5.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/remark-gfm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", - "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-gfm": "^2.0.0", - "micromark-extension-gfm": "^2.0.0", - "unified": "^10.0.0" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.8" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "ms": "2.0.0" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0" + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "engines": { + "node": ">=4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile-location": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", - "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", + "license": "MIT" + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 14.17" } }, - "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" } }, - "node_modules/dom-converter": { + "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "node": ">= 0.6" } }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, "engines": { - "node": ">=10" + "node": "*" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, - "node_modules/dot-prop/node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=14.14" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 4" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/emoticon": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", - "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=6.9.0" } }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", "engines": { - "node": ">=0.12" + "node": ">=10" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { - "is-arrayish": "^0.2.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, "engines": { - "node": ">= 0.4" + "node": ">= 6" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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": { - "es-errors": "^1.3.0" + "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": ">= 0.4" + "node": "*" } }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "license": "MIT" - }, - "node_modules/esast-util-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", - "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0" + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esast-util-from-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", - "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" + "lodash": "^4.17.15" } }, - "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" + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "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": ">=4" + "node": ">=6.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", + "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": { - "estraverse": "^5.2.0" + "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_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" }, "engines": { - "node": ">=4.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=8" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estree-util-attach-comments": { + "node_modules/has-yarn": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", "license": "MIT", "dependencies": { + "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/estree-util-to-js/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", - "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/remcohaszing" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eta": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", - "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" }, "funding": { - "url": "https://github.com/eta-dev/eta?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "bin": { + "he": "bin/he" } }, - "node_modules/eval": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", - "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", "dependencies": { - "@types/node": "*", - "require-like": ">= 0.1.1" - }, - "engines": { - "node": ">= 0.8" + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "license": "MIT", - "engines": { - "node": ">=0.8.x" + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" } }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", - "engines": { - "node": ">=18.0.0" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "safe-buffer": "~5.1.0" } }, - "node_modules/exenv": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", - "license": "BSD-3-Clause" + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" }, - "engines": { - "node": ">= 0.10.0" + "bin": { + "html-minifier-terser": "cli.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": "^14.13.1 || >=16.0.0" } }, - "node_modules/express/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=14" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/html-webpack-plugin": { + "version": "5.6.6", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", + "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" }, "engines": { - "node": ">=8.6.0" + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", { "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" + "url": "https://github.com/sponsors/fb55" } ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" } }, - "node_modules/fault": { + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "format": "^0.2.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", "dependencies": { - "websocket-driver": ">=0.5.1" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" }, "engines": { - "node": ">=0.8.0" + "node": ">=8.0.0" } }, - "node_modules/feed": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", - "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "license": "MIT", "dependencies": { - "xml-js": "^1.6.11" + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" }, "engines": { - "node": ">=0.4.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, "engines": { - "node": ">=8" + "node": ">=10" }, "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/http-reasons": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", + "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", + "license": "Apache-2.0" }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "license": "MIT" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "license": "MIT", "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "node": ">=10.19.0" } }, - "node_modules/file-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==", + "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": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "agent-base": "^7.1.2", + "debug": "4" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" + "engines": { + "node": ">= 14" } }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=10.17.0" } }, - "node_modules/file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", - "license": "MIT" - }, - "node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=10.18" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 4" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", "license": "MIT" }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", "license": "MIT", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/immer" } }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=8" } }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", - "license": "MIT" - }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", "engines": { - "node": ">= 14.17" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" + "node": ">=0.8.19" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", "license": "MIT", "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" + "node": ">=12" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", "engines": { - "node": ">=14.14" + "node": ">=10" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.10" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "loose-envify": "^1.0.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">= 10" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "license": "ISC" + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "ci-info": "^3.2.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "is-ci": "bin.js" } }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "license": "ISC" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "hasown": "^2.0.2" }, "engines": { - "node": "*" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", - "dependencies": { - "ini": "2.0.0" + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "license": "ISC", + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/got/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", - "engines": { - "node": ">=14.16" - }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "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/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "license": "MIT", "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" }, "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": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, "bin": { - "js-yaml": "bin/js-yaml.js" + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "license": "MIT", "dependencies": { - "duplexer": "^0.1.2" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" }, "engines": { "node": ">=10" @@ -11052,2080 +11478,2243 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.12.0" } }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" + "engines": { + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0" + "isobject": "^3.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/hast-util-to-estree": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", - "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" + "engines": { + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" + "is-docker": "^2.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8" } }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", - "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", "bin": { - "he": "bin/he" - } - }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "jiti": "bin/jiti.js" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "license": "BSD-3-Clause", "dependencies": { - "react-is": "^16.7.0" + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "license": "MIT" }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/json-crawl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/json-crawl/-/json-crawl-0.5.3.tgz", + "integrity": "sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA==", "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "node_modules/json-pointer": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", + "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", "license": "MIT", "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" + "foreach": "^2.0.4" } }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", "license": "MIT", - "engines": { - "node": ">= 12" + "dependencies": { + "lodash": "^4.17.4" } }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "node_modules/json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=12.0.0" } }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "node_modules/html-webpack-plugin": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", - "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "license": "MIT", "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" + "universalify": "^2.0.0" }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "json-buffer": "3.0.1" } }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" + "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/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "license": "MIT" + "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/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "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": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "package-json": "^8.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=14.16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "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": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" } }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/http-reasons": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", - "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", - "license": "Apache-2.0" - }, - "node_modules/http2-client": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", - "license": "MIT" - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "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", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, "engines": { - "node": ">=10.19.0" + "node": ">=6" } }, - "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==", + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, "engines": { - "node": ">= 14" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/liquid-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", + "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", "license": "Apache-2.0", "engines": { - "node": ">=10.17.0" + "node": ">=4" } }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", "engines": { - "node": ">=10.18" + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.9.0" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "license": "ISC", + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "postcss": "^8.1.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" }, - "node_modules/immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "tslib": "^2.0.3" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/infima": { - "version": "0.2.0-alpha.45", - "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", - "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "node_modules/lunr-languages": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.14.0.tgz", + "integrity": "sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA==", + "license": "MPL-1.1" }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "license": "MIT" }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 0.4" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" + "engines": { + "node": ">=12" }, - "bin": { - "is-ci": "bin.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-decimal": { + "node_modules/mdast-util-gfm-autolink-literal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "license": "MIT", "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-inside-container/node_modules/is-docker": { + "node_modules/mdast-util-mdx": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", - "engines": { - "node": ">=16" + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-npm": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", - "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", - "engines": { - "node": ">=0.12.0" + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "@types/mdast": "^4.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", + "node_modules/memfs": { + "version": "4.56.10", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", + "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", + "license": "Apache-2.0", "dependencies": { - "is-docker": "^2.0.0" + "@jsonjoy.com/fs-core": "4.56.10", + "@jsonjoy.com/fs-fsa": "4.56.10", + "@jsonjoy.com/fs-node": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.10", + "@jsonjoy.com/fs-node-to-fsa": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-print": "4.56.10", + "@jsonjoy.com/fs-snapshot": "4.56.10", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/isexe": { + "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.6" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "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==", + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-crawl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/json-crawl/-/json-crawl-0.5.3.tgz", - "integrity": "sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA==", + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "foreach": "^2.0.4" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/json-schema-compare": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", - "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", "license": "MIT", "dependencies": { - "lodash": "^4.17.4" + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/json-schema-merge-allof": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", - "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "compute-lcm": "^1.1.2", - "json-schema-compare": "^0.2.2", - "lodash": "^4.17.20" - }, - "engines": { - "node": ">=12.0.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "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==", + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.11" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "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/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/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==", + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", "license": "MIT", "dependencies": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "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==", + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "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==", + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", "license": "MIT", - "engines": { - "node": ">=14" + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/liquid-json": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", - "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", - "engines": { - "node": ">=6.11.5" + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/unified" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, - "bin": { - "loose-envify": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "tslib": "^2.0.3" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", "dependencies": { - "yallist": "^3.0.2" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "license": "MIT" - }, - "node_modules/lunr-languages": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.14.0.tgz", - "integrity": "sha512-hWUAb2KqM3L7J5bcrngszzISY4BxrXn/Xhbb9TTCJYEGqlR1nG67/M14sp09+PTIRklobrn57IAxcdcO/ZFyNA==", - "license": "MPL-1.1" - }, - "node_modules/mark.js": { - "version": "8.11.1", - "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "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==", + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-definitions/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-directive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", - "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^6.0.0" + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", + "@types/estree": "^1.0.0", "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-frontmatter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", - "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "escape-string-regexp": "^5.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-gfm-autolink-literal": { + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", + "@types/estree": "^1.0.0", "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" } }, - "node_modules/mdast-util-gfm-table/node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-mdx-expression": { + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdast-util-mdxjs-esm": { + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "4.51.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", - "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", - "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13138,29 +13727,16 @@ ], "license": "MIT", "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -13173,184 +13749,142 @@ ], "license": "MIT", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-frontmatter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", - "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "fault": "^2.0.0", - "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" } }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", - "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13363,96 +13897,14 @@ ], "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", - "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "license": "MIT", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13463,17 +13915,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } + "license": "MIT" }, - "node_modules/micromark-factory-label": { + "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -13486,16 +13933,14 @@ ], "license": "MIT", "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", + "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", - "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -13508,21 +13953,13 @@ ], "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/micromark-factory-space": { + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13533,16 +13970,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } + "license": "MIT" }, - "node_modules/micromark-factory-title": { + "node_modules/micromark-util-decode-string": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13555,16 +13988,16 @@ ], "license": "MIT", "dependencies": { - "micromark-factory-space": "^2.0.0", + "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13577,16 +14010,14 @@ ], "license": "MIT", "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13597,16 +14028,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } + "license": "MIT" }, - "node_modules/micromark-util-chunked": { + "node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -13617,15 +14044,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } + "license": "MIT" }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", "funding": [ { "type": "GitHub Sponsors", @@ -13638,15 +14062,19 @@ ], "license": "MIT", "dependencies": { - "micromark-util-character": "^2.0.0", + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" } }, - "node_modules/micromark-util-combine-extensions": { + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13657,16 +14085,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } + "license": "MIT" }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -13677,15 +14101,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } + "license": "MIT" }, - "node_modules/micromark-util-decode-string": { + "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13698,16 +14119,13 @@ ], "license": "MIT", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, - "node_modules/micromark-util-encode": { + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13720,10 +14138,10 @@ ], "license": "MIT" }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", - "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -13736,1732 +14154,1329 @@ ], "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" + "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-util-html-tag-name": { + "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", "url": "https://github.com/sponsors/unifiedjs" }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-format": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.1.tgz", - "integrity": "sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==", - "license": "Apache-2.0", - "dependencies": { - "charset": "^1.0.0" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", - "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "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/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/neotraverse": { - "version": "0.6.15", - "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.15.tgz", - "integrity": "sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "http2-client": "^1.2.5" - }, - "engines": { - "node": "4.x || >=6.0.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "es6-promise": "^3.2.1" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "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==", + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/normalize-url": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", - "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "license": "MIT" - }, - "node_modules/nth-check": { + "node_modules/micromark/node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/null-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", - "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "node": ">=8.6" } }, - "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==", + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "bin": { + "mime": "cli.js" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=10.0.0" } }, - "node_modules/null-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" + "engines": { + "node": ">= 0.6" } }, - "node_modules/null-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" + "node_modules/mime-format": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.2.tgz", + "integrity": "sha512-Y5ERWVcyh3sby9Fx2U5F1yatiTFjNsqF5NltihTWI9QgNtr5o3dbCZdcKa1l2wyfhnwwoP9HGNxga7LqZLA6gw==", + "license": "Apache-2.0", + "dependencies": { + "charset": "^1.0.0" + } }, - "node_modules/null-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "mime-db": "~1.33.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">= 0.6" } }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", - "license": "BSD-3-Clause", - "dependencies": { - "fast-safe-stringify": "^2.0.7" + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/oas-linter": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", - "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", - "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", - "license": "BSD-3-Clause", + "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==", + "license": "MIT", "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, - "bin": { - "resolve": "resolve.js" + "engines": { + "node": ">= 12.13.0" }, "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver-browser": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz", - "integrity": "sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==", - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "path-browserify": "^1.0.1", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" + "peerDependencies": { + "webpack": "^5.0.0" } }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" }, - "node_modules/oas-validator": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", - "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", - "license": "BSD-3-Clause", + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" + "brace-expansion": "^2.0.1" }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" + "engines": { + "node": ">=10" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=10" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "multicast-dns": "cli.js" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "bin": { + "mustache": "bin/mustache" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/neotraverse": { + "version": "0.6.15", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.15.tgz", + "integrity": "sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 10" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", "dependencies": { - "wrappy": "1" + "lower-case": "^2.0.2", + "tslib": "^2.0.3" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-to-postmanv2": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-4.25.0.tgz", - "integrity": "sha512-sIymbkQby0gzxt2Yez8YKB6hoISEel05XwGwNrAhr6+vxJWXNxkmssQc/8UEtVkuJ9ZfUXLkip9PYACIpfPDWg==", - "license": "Apache-2.0", - "dependencies": { - "ajv": "8.11.0", - "ajv-draft-04": "1.0.0", - "ajv-formats": "2.1.1", - "async": "3.2.4", - "commander": "2.20.3", - "graphlib": "2.1.8", - "js-yaml": "4.1.0", - "json-pointer": "0.6.2", - "json-schema-merge-allof": "0.8.1", - "lodash": "4.17.21", - "neotraverse": "0.6.15", - "oas-resolver-browser": "2.5.6", - "object-hash": "3.0.0", - "path-browserify": "1.0.1", - "postman-collection": "^4.4.0", - "swagger2openapi": "7.0.8", - "yaml": "1.10.2" + "node": "4.x || >=6.0.0" }, - "bin": { - "openapi2postmanv2": "bin/openapi2postmanv2.js" + "peerDependencies": { + "encoding": "^0.1.0" }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/openapi-to-postmanv2/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "http2-client": "^1.2.5" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": "4.x || >=6.0.0" } }, - "node_modules/openapi-to-postmanv2/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "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==", + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", "license": "MIT", "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "es6-promise": "^3.2.1" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } + "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" }, - "node_modules/p-cancelable": { + "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">=0.10.0" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" + "path-key": "^3.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", "license": "MIT", "dependencies": { - "p-limit": "^4.0.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "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==", "license": "MIT", "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "license": "MIT", "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">=8" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "license": "MIT", + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "license": "BSD-3-Clause", "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "fast-safe-stringify": "^2.0.7" } }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "license": "BSD-3-Clause", "dependencies": { - "p-finally": "^1.0.0" + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/package-json": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", - "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", - "license": "MIT", + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "license": "BSD-3-Clause", "dependencies": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" }, - "engines": { - "node": ">=14.16" + "bin": { + "resolve": "resolve.js" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/package-json/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", + "node_modules/oas-resolver-browser": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz", + "integrity": "sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "path-browserify": "^1.0.1", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, "bin": { - "semver": "bin/semver.js" + "resolve": "resolve.js" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "license": "MIT", + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "license": "BSD-3-Clause", "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">= 6" } }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/parse-numeric-range": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", - "license": "ISC" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { - "parse5": "^7.0.0" + "ee-first": "1.1.1" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">= 0.8" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "license": "MIT", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "wrappy": "1" } }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", + "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==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.11.0", + "ajv-draft-04": "1.0.0", + "ajv-formats": "2.1.1", + "async": "3.2.6", + "commander": "2.20.3", + "graphlib": "2.1.8", + "js-yaml": "4.1.0", + "json-pointer": "0.6.2", + "json-schema-merge-allof": "0.8.1", + "lodash": "4.17.21", + "neotraverse": "0.6.15", + "oas-resolver-browser": "2.5.6", + "object-hash": "3.0.0", + "path-browserify": "1.0.1", + "postman-collection": "^5.0.0", + "swagger2openapi": "7.0.8", + "yaml": "1.10.2" + }, + "bin": { + "openapi2postmanv2": "bin/openapi2postmanv2.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "license": "(WTFPL OR MIT)" + "node_modules/openapi-to-postmanv2/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "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", - "engines": { - "node": ">=8" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", - "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "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==", "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=12.20" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", "license": "MIT", "dependencies": { - "find-up": "^6.3.0" + "yocto-queue": "^1.0.0" }, "engines": { - "node": ">=14.16" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, "engines": { - "node": ">=4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "aggregate-error": "^3.0.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", - "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.0.0" + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0" + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=16.17" }, - "peerDependencies": { - "postcss": "^8.2.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-calc/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==", + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "p-finally": "^1.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" }, "engines": { - "node": ">=7.6.0" + "node": ">=14.16" }, - "peerDependencies": { - "postcss": "^8.4.6" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-color-functional-notation": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", - "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "dot-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/postcss-color-hex-alpha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", - "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "license": "MIT", "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" + "callsites": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=6" } }, - "node_modules/postcss-color-rebeccapurple": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", - "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss-colormin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", - "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=8" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-convert-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", - "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" + "entities": "^6.0.0" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/postcss-custom-media": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", - "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" + "domhandler": "^5.0.3", + "parse5": "^7.0.0" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/postcss-custom-properties": { - "version": "14.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", - "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" + "parse5": "^7.0.0" }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=0.12" }, - "peerDependencies": { - "postcss": "^8.4" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/postcss-custom-selectors": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", - "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "postcss-selector-parser": "^7.0.0" - }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">= 0.8" } }, - "node_modules/postcss-dir-pseudo-class": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", - "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/postcss-discard-comments": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", - "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", - "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-discard-empty": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", - "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=8.6" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/postcss-discard-unused": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", - "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">= 6" } }, - "node_modules/postcss-discard-unused/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==", + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "find-up": "^6.3.0" }, "engines": { - "node": ">=4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-double-position-gradients": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", - "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/pkijs": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", + "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", + "license": "BSD-3-Clause", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=16.0.0" } }, - "node_modules/postcss-focus-visible": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", - "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/csstools" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" }, { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.0.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-focus-within": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", - "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", "funding": [ { "type": "github", @@ -15472,7 +15487,7 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", "dependencies": { "postcss-selector-parser": "^7.0.0" }, @@ -15483,67 +15498,54 @@ "postcss": "^8.4" } }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "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==", "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.0" + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "node_modules/postcss-gap-properties": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", - "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=18" + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.4" + "postcss": "^8.2.2" } }, - "node_modules/postcss-image-set-function": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", - "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", "dependencies": { - "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=18" + "node": ">=7.6.0" }, "peerDependencies": { - "postcss": "^8.4" + "postcss": "^8.4.6" } }, - "node_modules/postcss-lab-function": { + "node_modules/postcss-color-functional-notation": { "version": "7.0.12", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", - "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", "funding": [ { "type": "github", @@ -15569,44 +15571,36 @@ "postcss": "^8.4" } }, - "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" }, "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "postcss": "^8.4" } }, - "node_modules/postcss-logical": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", - "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", "funding": [ { "type": "github", @@ -15619,6 +15613,7 @@ ], "license": "MIT-0", "dependencies": { + "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -15628,109 +15623,15 @@ "postcss": "^8.4" } }, - "node_modules/postcss-merge-idents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", - "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", - "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", - "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.2", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules/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==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", - "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", - "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-params": { + "node_modules/postcss-colormin": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", - "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", "license": "MIT", "dependencies": { "browserslist": "^4.23.0", - "cssnano-utils": "^4.0.2", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -15740,13 +15641,14 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-minify-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", - "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.16" + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^14 || ^16 || >=18.0" @@ -15755,82 +15657,108 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-minify-selectors/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==", + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" }, "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.1.0" + "postcss": "^8.4" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.1.0" + "postcss": "^8.4" } }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "license": "ISC", + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.1.0" + "postcss": "^8.4" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "license": "ISC", + "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==", + "license": "MIT", "dependencies": { - "icss-utils": "^5.0.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=4" } }, - "node_modules/postcss-nesting": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", - "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", "funding": [ { "type": "github", @@ -15843,8 +15771,6 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^3.1.0", - "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, "engines": { @@ -15854,41 +15780,24 @@ "postcss": "^8.4" } }, - "node_modules/postcss-normalize-charset": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", - "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", - "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "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==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=4" } }, - "node_modules/postcss-normalize-positions": { + "node_modules/postcss-discard-comments": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", - "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { "node": "^14 || ^16 || >=18.0" }, @@ -15896,14 +15805,11 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", - "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { "node": "^14 || ^16 || >=18.0" }, @@ -15911,14 +15817,11 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-normalize-string": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", - "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { "node": "^14 || ^16 || >=18.0" }, @@ -15926,30 +15829,11 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-normalize-timing-functions": { + "node_modules/postcss-discard-overridden": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", - "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", - "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, "engines": { "node": "^14 || ^16 || >=18.0" }, @@ -15957,13 +15841,13 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-normalize-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", - "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "postcss-selector-parser": "^6.0.16" }, "engines": { "node": "^14 || ^16 || >=18.0" @@ -15972,36 +15856,51 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-normalize-whitespace": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", - "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", - "license": "MIT", + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-opacity-percentage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", - "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", "funding": [ { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" + "type": "github", + "url": "https://github.com/sponsors/csstools" }, { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" + "type": "opencollective", + "url": "https://opencollective.com/csstools" } ], - "license": "MIT", + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, "engines": { "node": ">=18" }, @@ -16009,26 +15908,23 @@ "postcss": "^8.4" } }, - "node_modules/postcss-ordered-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", - "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "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==", "license": "MIT", "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=4" } }, - "node_modules/postcss-overflow-shorthand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", - "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", "funding": [ { "type": "github", @@ -16041,7 +15937,7 @@ ], "license": "MIT-0", "dependencies": { - "postcss-value-parser": "^4.2.0" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": ">=18" @@ -16050,19 +15946,32 @@ "postcss": "^8.4" } }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "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==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", "license": "MIT", "peerDependencies": { - "postcss": "^8" + "postcss": "^8.1.0" } }, - "node_modules/postcss-place": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", - "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", "funding": [ { "type": "github", @@ -16074,9 +15983,6 @@ } ], "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { "node": ">=18" }, @@ -16084,10 +15990,10 @@ "postcss": "^8.4" } }, - "node_modules/postcss-preset-env": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.6.0.tgz", - "integrity": "sha512-+LzpUSLCGHUdlZ1YZP7lp7w1MjxInJRSG0uaLyk/V/BM17iU2B7xTO7I8x3uk0WQAcLLh/ffqKzOzfaBvG7Fdw==", + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", "funding": [ { "type": "github", @@ -16100,77 +16006,8 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-alpha-function": "^1.0.1", - "@csstools/postcss-cascade-layers": "^5.0.2", - "@csstools/postcss-color-function": "^4.0.12", - "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", - "@csstools/postcss-color-mix-function": "^3.0.12", - "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", - "@csstools/postcss-content-alt-text": "^2.0.8", - "@csstools/postcss-contrast-color-function": "^2.0.12", - "@csstools/postcss-exponential-functions": "^2.0.9", - "@csstools/postcss-font-format-keywords": "^4.0.0", - "@csstools/postcss-gamut-mapping": "^2.0.11", - "@csstools/postcss-gradients-interpolation-method": "^5.0.12", - "@csstools/postcss-hwb-function": "^4.0.12", - "@csstools/postcss-ic-unit": "^4.0.4", - "@csstools/postcss-initial": "^2.0.1", - "@csstools/postcss-is-pseudo-class": "^5.0.3", - "@csstools/postcss-light-dark-function": "^2.0.11", - "@csstools/postcss-logical-float-and-clear": "^3.0.0", - "@csstools/postcss-logical-overflow": "^2.0.0", - "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", - "@csstools/postcss-logical-resize": "^3.0.0", - "@csstools/postcss-logical-viewport-units": "^3.0.4", - "@csstools/postcss-media-minmax": "^2.0.9", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", - "@csstools/postcss-nested-calc": "^4.0.0", - "@csstools/postcss-normalize-display-values": "^4.0.0", - "@csstools/postcss-oklab-function": "^4.0.12", - "@csstools/postcss-position-area-property": "^1.0.0", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/postcss-property-rule-prelude-list": "^1.0.0", - "@csstools/postcss-random-function": "^2.0.1", - "@csstools/postcss-relative-color-syntax": "^3.0.12", - "@csstools/postcss-scope-pseudo-class": "^4.0.1", - "@csstools/postcss-sign-functions": "^1.1.4", - "@csstools/postcss-stepped-value-functions": "^4.0.9", - "@csstools/postcss-syntax-descriptor-syntax-production": "^1.0.1", - "@csstools/postcss-system-ui-font-family": "^1.0.0", - "@csstools/postcss-text-decoration-shorthand": "^4.0.3", - "@csstools/postcss-trigonometric-functions": "^4.0.9", - "@csstools/postcss-unset-value": "^4.0.0", - "autoprefixer": "^10.4.23", - "browserslist": "^4.28.1", - "css-blank-pseudo": "^7.0.1", - "css-has-pseudo": "^7.0.3", - "css-prefers-color-scheme": "^10.0.0", - "cssdb": "^8.6.0", - "postcss-attribute-case-insensitive": "^7.0.1", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.12", - "postcss-color-hex-alpha": "^10.0.0", - "postcss-color-rebeccapurple": "^10.0.0", - "postcss-custom-media": "^11.0.6", - "postcss-custom-properties": "^14.0.6", - "postcss-custom-selectors": "^8.0.5", - "postcss-dir-pseudo-class": "^9.0.1", - "postcss-double-position-gradients": "^6.0.4", - "postcss-focus-visible": "^10.0.1", - "postcss-focus-within": "^9.0.1", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^6.0.0", - "postcss-image-set-function": "^7.0.0", - "postcss-lab-function": "^7.0.12", - "postcss-logical": "^8.1.0", - "postcss-nesting": "^13.0.2", - "postcss-opacity-percentage": "^3.0.0", - "postcss-overflow-shorthand": "^6.0.0", - "postcss-page-break": "^3.0.4", - "postcss-place": "^10.0.0", - "postcss-pseudo-class-any-link": "^10.0.1", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^8.0.1" + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" }, "engines": { "node": ">=18" @@ -16179,10 +16016,10 @@ "postcss": "^8.4" } }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", - "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", "funding": [ { "type": "github", @@ -16195,7 +16032,11 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^7.0.0" + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" }, "engines": { "node": ">=18" @@ -16204,43 +16045,60 @@ "postcss": "^8.4" } }, - "node_modules/postcss-reduce-idents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", - "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" } }, - "node_modules/postcss-reduce-initial": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", - "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", - "license": "MIT", + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-reduce-transforms": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", - "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", "license": "MIT", "dependencies": { + "cssnano-utils": "^4.0.2", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -16250,88 +16108,93 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", - "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", "license": "MIT", "dependencies": { - "postcss-selector-parser": "^7.0.0" + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" }, "engines": { - "node": ">=18" + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.4" + "postcss": "^8.4.31" } }, - "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==", + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=4" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/postcss-sort-media-queries": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", - "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", "license": "MIT", "dependencies": { - "sort-css-media-queries": "2.2.0" + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=14.0.0" + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.4.23" + "postcss": "^8.4.31" } }, - "node_modules/postcss-svgo": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", - "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >= 18" + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { "postcss": "^8.4.31" } }, - "node_modules/postcss-unique-selectors": { + "node_modules/postcss-minify-selectors": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", - "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.16" @@ -16343,1257 +16206,1239 @@ "postcss": "^8.4.31" } }, - "node_modules/postcss-unique-selectors/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==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", "engines": { - "node": ">=4" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/postcss-value-parser": { + "node_modules/postcss-modules-local-by-default": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/postcss-zindex": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", - "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^10 || ^12 || >= 14" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.1.0" } }, - "node_modules/postman-code-generators": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/postman-code-generators/-/postman-code-generators-1.14.2.tgz", - "integrity": "sha512-qZAyyowfQAFE4MSCu2KtMGGQE/+oG1JhMZMJNMdZHYCSfQiVVeKxgk3oI4+KJ3d1y5rrm2D6C6x+Z+7iyqm+fA==", - "hasInstallScript": true, - "license": "Apache-2.0", + "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==", + "license": "MIT", "dependencies": { - "async": "3.2.2", - "detect-package-manager": "3.0.2", - "lodash": "4.17.21", - "path": "0.12.7", - "postman-collection": "^4.4.0", - "shelljs": "0.8.5" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/postman-code-generators/node_modules/async": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", - "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", - "license": "MIT" - }, - "node_modules/postman-collection": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.5.0.tgz", - "integrity": "sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==", - "license": "Apache-2.0", + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", "dependencies": { - "@faker-js/faker": "5.5.3", - "file-type": "3.9.0", - "http-reasons": "0.1.0", - "iconv-lite": "0.6.3", - "liquid-json": "0.3.1", - "lodash": "4.17.21", - "mime-format": "2.0.1", - "mime-types": "2.1.35", - "postman-url-encoder": "3.0.5", - "semver": "7.6.3", - "uuid": "8.3.2" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=10" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/postman-collection/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "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==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/postman-collection/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "icss-utils": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/postman-url-encoder": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz", - "integrity": "sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==", - "license": "Apache-2.0", + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "punycode": "^2.1.1" + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/pretty-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", - "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", - "license": "MIT", + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": ">=4" + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" } }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" }, "peerDependencies": { - "react": ">=16.0.0" + "postcss-selector-parser": "^7.0.0" } }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "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==", "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", "license": "MIT", "engines": { - "node": ">= 0.6.0" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", "license": "MIT", "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "license": "ISC" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=6" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/pupa": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", - "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", "license": "MIT", "dependencies": { - "escape-goat": "^4.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=12.20" + "node": "^14 || ^16 || >=18.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "license": "BSD-3-Clause", + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", "dependencies": { - "side-channel": "^1.1.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=0.6" + "node": "^14 || ^16 || >=18.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=10" + "node": "^14 || ^16 || >=18.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", "license": "MIT", "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">= 0.6" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "node": "^14 || ^16 || >=18.0" }, - "bin": { - "rc": "cli.js" + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "scheduler": "^0.27.0" + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "react": "^19.2.3" + "postcss": "^8.4" } }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "license": "MIT" + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } }, - "node_modules/react-helmet-async": { - "name": "@slorber/react-helmet-async", - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", - "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", - "license": "Apache-2.0", + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "dependencies": { - "@babel/runtime": "^7.12.5", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.2.0", - "shallowequal": "^1.1.0" + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "postcss": "^8.4" } }, - "node_modules/react-hook-form": { - "version": "7.71.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.0.tgz", - "integrity": "sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" + "node_modules/postcss-preset-env": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.6.1.tgz", + "integrity": "sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.1", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-position-area-property": "^1.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-property-rule-prelude-list": "^1.0.0", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-syntax-descriptor-syntax-production": "^1.0.1", + "@csstools/postcss-system-ui-font-family": "^1.0.0", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.23", + "browserslist": "^4.28.1", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.6.0", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" + "engines": { + "node": ">=18" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" + "postcss": "^8.4" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-json-view-lite": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", - "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", - "license": "MIT", + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, "engines": { "node": ">=18" }, "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" + "postcss": "^8.4" } }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" - }, - "node_modules/react-live": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/react-live/-/react-live-4.1.8.tgz", - "integrity": "sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg==", + "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==", "license": "MIT", "dependencies": { - "prism-react-renderer": "^2.4.0", - "sucrase": "^3.35.0", - "use-editable": "^2.3.3" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">= 0.12.0", - "npm": ">= 2.0.0" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "node": ">=4" } }, - "node_modules/react-loadable": { - "name": "@docusaurus/react-loadable", - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", - "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", "license": "MIT", "dependencies": { - "@types/react": "*" + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "react": "*" + "postcss": "^8.4.31" } }, - "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==", + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.3" + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" }, "engines": { - "node": ">=10.13.0" + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "react-loadable": "*", - "webpack": ">=4.41.1 || 5.x" + "postcss": "^8.4.31" } }, - "node_modules/react-magic-dropzone": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz", - "integrity": "sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==", - "license": "MIT" - }, - "node_modules/react-markdown": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", - "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "@types/prop-types": "^15.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "prop-types": "^15.0.0", - "property-information": "^6.0.0", - "react-is": "^18.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" + "postcss-value-parser": "^4.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "postcss": "^8.4.31" } }, - "node_modules/react-markdown/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", "license": "MIT", - "dependencies": { - "@types/unist": "^2" + "peerDependencies": { + "postcss": "^8.0.3" } }, - "node_modules/react-markdown/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@types/unist": "^2" + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/react-markdown/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/react-markdown/node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "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==", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "node_modules/react-markdown/node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", - "license": "MIT" - }, - "node_modules/react-markdown/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "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==", "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=4" } }, - "node_modules/react-markdown/node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "sort-css-media-queries": "2.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" } }, - "node_modules/react-markdown/node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", - "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/react-markdown/node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0" + "postcss-selector-parser": "^6.0.16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/react-markdown/node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/react-markdown/node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", + "node_modules/postman-code-generators": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postman-code-generators/-/postman-code-generators-2.1.0.tgz", + "integrity": "sha512-PCptfRoq6pyyqeB9qw87MfjpIZEZIykIna7Api9euhYftyrad/kCkIyXfWF6GrkcHv0nYid05xoRPWPX9JHkZg==", + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "async": "3.2.2", + "detect-package-manager": "3.0.2", + "lodash": "4.17.21", + "path": "0.12.7", + "postman-collection": "^5.0.0", + "shelljs": "0.8.5" + }, + "engines": { + "node": ">=18" } }, - "node_modules/react-markdown/node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", + "node_modules/postman-code-generators/node_modules/async": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "license": "MIT" + }, + "node_modules/postman-code-generators/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/postman-collection": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-5.2.1.tgz", + "integrity": "sha512-KWzsR1RdLYuufabEEZ+UaMn/exDUNkGqC7tT8GkWumarGdpl/dAh3Lcgo7Z2fDqsGeb+EkqZgrYH8beXRtLmjA==", + "license": "Apache-2.0", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.23", + "mime": "3.0.0", + "mime-format": "2.0.2", + "postman-url-encoder": "3.0.8", + "semver": "7.7.1", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postman-collection/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/react-markdown/node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", + "node_modules/postman-url-encoder": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.8.tgz", + "integrity": "sha512-EOgUMBazo7JNP4TDrd64TsooCiWzzo4143Ws8E8WYGEpn2PKpq+S4XRTDhuRTYHm3VKOpUZs7ZYZq7zSDuesqA==", + "license": "Apache-2.0", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=10" } }, - "node_modules/react-markdown/node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "lodash": "^4.17.20", + "renderkid": "^3.0.0" } }, - "node_modules/react-markdown/node_modules/micromark-factory-title": { + "node_modules/pretty-time": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "engines": { + "node": ">=4" } }, - "node_modules/react-markdown/node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", "license": "MIT", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" } }, - "node_modules/react-markdown/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/react-markdown/node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" + "engines": { + "node": ">= 0.6.0" } }, - "node_modules/react-markdown/node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/react-markdown/node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/react-markdown/node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/react-markdown/node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/react-markdown/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/react-markdown/node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, - "node_modules/react-markdown/node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/react-markdown/node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", "license": "MIT", "dependencies": { - "micromark-util-types": "^1.0.0" + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-markdown/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "tslib": "^2.8.1" } }, - "node_modules/react-markdown/node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/react-markdown/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "funding": [ { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/react-markdown/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "type": "patreon", + "url": "https://www.patreon.com/feross" }, { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "type": "consulting", + "url": "https://feross.org/support" } ], "license": "MIT" }, - "node_modules/react-markdown/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-markdown/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/react-markdown/node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.8" } }, - "node_modules/react-markdown/node_modules/remark-rehype": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", - "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" + "safer-buffer": ">= 2.1.2 < 3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/react-markdown/node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", - "license": "MIT", + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { - "inline-style-parser": "0.1.1" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/react-markdown/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" + "scheduler": "^0.27.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "peerDependencies": { + "react": "^19.2.4" } }, - "node_modules/react-markdown/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", "dependencies": { - "@types/unist": "^2.0.0" + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-markdown/node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "node_modules/react-hook-form": { + "version": "7.71.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", + "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" + "engines": { + "node": ">=18.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" } }, - "node_modules/react-markdown/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" + "engines": { + "node": ">=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" } }, - "node_modules/react-markdown/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-live": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/react-live/-/react-live-4.1.8.tgz", + "integrity": "sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "prism-react-renderer": "^2.4.0", + "sucrase": "^3.35.0", + "use-editable": "^2.3.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.12.0", + "npm": ">= 2.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, - "node_modules/react-markdown/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/react": "*" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "peerDependencies": { + "react": "*" } }, - "node_modules/react-markdown/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "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==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "@babel/runtime": "^7.10.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" } }, - "node_modules/react-markdown/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "node_modules/react-magic-dropzone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz", + "integrity": "sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" } }, "node_modules/react-modal": { @@ -17612,6 +17457,29 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", @@ -17663,15 +17531,6 @@ "react": ">=15" } }, - "node_modules/react-router/node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "license": "MIT", - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -17777,23 +17636,26 @@ } }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" }, "node_modules/redux-thunk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", "peerDependencies": { - "redux": "^4" + "redux": "^5.0.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reftools": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", @@ -17839,12 +17701,12 @@ } }, "node_modules/registry-auth-token": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", - "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "license": "MIT", "dependencies": { - "@pnpm/npm-conf": "^2.1.0" + "@pnpm/npm-conf": "^3.0.2" }, "engines": { "node": ">=14" @@ -18192,9 +18054,9 @@ "license": "MIT" }, "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, "node_modules/resolve": { @@ -18325,18 +18187,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -18364,9 +18214,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz", - "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -18384,9 +18234,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.6.tgz", - "integrity": "sha512-sglGzId5gmlfxNs4gK2U3h7HlVRfx278YK6Ono5lwzuvi1jxig80YiuHkaDBVsYIKFhx8wN7XSCI0M2IDS/3qA==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.7.tgz", + "integrity": "sha512-w6q+fRHourZ+e+xA1kcsF27iGM6jdB8teexYCfdUw0sYgcDNeZESnDNT9sUmmPm3ooziwUJXGwZJSTF3kOdBfA==", "license": "MIT", "dependencies": { "neo-async": "^2.6.2" @@ -18399,7 +18249,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", @@ -18452,10 +18302,13 @@ } }, "node_modules/sax": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", - "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "license": "BlueOak-1.0.0" + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.27.0", @@ -18515,25 +18368,28 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/semver-diff": { @@ -18551,18 +18407,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semver-diff/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -18602,6 +18446,27 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -18626,61 +18491,54 @@ "range-parser": "1.2.0" } }, - "node_modules/serve-handler/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-handler/node_modules/mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "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", - "engines": { - "node": ">= 0.6" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/serve-handler/node_modules/mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "license": "MIT", + "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": { - "mime-db": "~1.33.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 0.6" + "node": "*" } }, - "node_modules/serve-handler/node_modules/range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } + "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", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-index/node_modules/debug": { @@ -18702,25 +18560,41 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", @@ -18728,12 +18602,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "license": "ISC" - }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -19087,12 +18955,12 @@ } }, "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">= 12" } }, "node_modules/source-map-js": { @@ -19114,6 +18982,15 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -19344,19 +19221,6 @@ "postcss": "^8.4.31" } }, - "node_modules/stylehacks/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==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -19389,18 +19253,15 @@ } }, "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -19482,19 +19343,6 @@ "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, - "node_modules/swr": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.8.tgz", - "integrity": "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -19509,9 +19357,9 @@ } }, "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -19560,6 +19408,35 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -19603,18 +19480,6 @@ "tslib": "^2" } }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -19771,6 +19636,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -19796,6 +19679,27 @@ "node": ">= 0.6" } }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -19820,9 +19724,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz", + "integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -19902,18 +19806,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unified/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -19929,16 +19821,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -19992,9 +19874,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -20142,18 +20024,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/update-notifier/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -20234,6 +20104,27 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, + "node_modules/url-loader/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/url-loader/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -20330,33 +20221,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/uvu/node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/validate.io-array": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", @@ -20457,9 +20321,9 @@ } }, "node_modules/watchpack": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", - "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -20495,9 +20359,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -20510,7 +20374,7 @@ "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", + "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -20523,7 +20387,7 @@ "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", + "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { @@ -20606,6 +20470,21 @@ } } }, + "node_modules/webpack-dev-middleware/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-dev-middleware/node_modules/mime-types": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", @@ -20622,15 +20501,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", + "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", @@ -20640,9 +20528,9 @@ "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", - "compression": "^1.7.4", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", + "express": "^4.22.1", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", @@ -20650,7 +20538,7 @@ "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", + "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", @@ -20679,6 +20567,12 @@ } } }, + "node_modules/webpack-dev-server/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -20710,9 +20604,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -20731,17 +20625,17 @@ } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { @@ -20753,6 +20647,27 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpackbar": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", @@ -20781,6 +20696,19 @@ "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", @@ -20839,6 +20767,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -20847,18 +20776,6 @@ "node": ">=18" } }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -21052,15 +20969,15 @@ } }, "node_modules/xml-formatter": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.1.tgz", - "integrity": "sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==", + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.7.tgz", + "integrity": "sha512-IsfFYJQuoDqtUlKhm4EzeoBOb+fQwzQVeyxxAQ0sThn/nFnQmyLPTplqq4yRhaOENH/tAyujD2TBfIYzUKB6hg==", "license": "MIT", "dependencies": { - "xml-parser-xo": "^3.2.0" + "xml-parser-xo": "^4.1.5" }, "engines": { - "node": ">= 10" + "node": ">= 16" } }, "node_modules/xml-js": { @@ -21076,12 +20993,12 @@ } }, "node_modules/xml-parser-xo": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz", - "integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.5.tgz", + "integrity": "sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 16" } }, "node_modules/y18n": { @@ -21173,15 +21090,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 7c85cc5a..fe4371d5 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -41,7 +41,7 @@ const sidebars: SidebarsConfig = { type: "category", label: "Plugins", link: { type: "doc", id: "plugins/index" }, - items: ["plugins/open-library"], + items: ["plugins/open-library", "plugins/anilist-sync"], }, { type: "category", diff --git a/migration/src/lib.rs b/migration/src/lib.rs index a38ba690..a466ed06 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -96,6 +96,9 @@ mod m20260203_000050_add_plugin_metadata_targets; // OIDC authentication pub mod m20260205_000051_create_oidc_connections; +// User plugin system (per-user plugin instances and data storage) +mod m20260205_000052_create_user_plugins; + pub struct Migrator; #[async_trait::async_trait] @@ -174,6 +177,8 @@ impl MigratorTrait for Migrator { Box::new(m20260203_000050_add_plugin_metadata_targets::Migration), // OIDC authentication Box::new(m20260205_000051_create_oidc_connections::Migration), + // User plugin system + Box::new(m20260205_000052_create_user_plugins::Migration), ] } } diff --git a/migration/src/m20260127_000035_create_plugins.rs b/migration/src/m20260127_000035_create_plugins.rs index b7b1ce6f..16f08dea 100644 --- a/migration/src/m20260127_000035_create_plugins.rs +++ b/migration/src/m20260127_000035_create_plugins.rs @@ -94,7 +94,7 @@ impl MigrationTrait for Migration { ColumnDef::new(Plugins::CredentialDelivery) .string_len(20) .not_null() - .default("env"), + .default("init_message"), ) // Plugin configuration .col( diff --git a/migration/src/m20260205_000052_create_user_plugins.rs b/migration/src/m20260205_000052_create_user_plugins.rs new file mode 100644 index 00000000..44cfc9ac --- /dev/null +++ b/migration/src/m20260205_000052_create_user_plugins.rs @@ -0,0 +1,284 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Create user_plugins table - per-user plugin instances + manager + .create_table( + Table::create() + .table(UserPlugins::Table) + .if_not_exists() + .col( + ColumnDef::new(UserPlugins::Id) + .uuid() + .not_null() + .primary_key(), + ) + // References + .col(ColumnDef::new(UserPlugins::PluginId).uuid().not_null()) + .col(ColumnDef::new(UserPlugins::UserId).uuid().not_null()) + // Per-user credentials (encrypted OAuth tokens, API keys) + .col(ColumnDef::new(UserPlugins::Credentials).binary()) + // Per-user configuration overrides + .col( + ColumnDef::new(UserPlugins::Config) + .json() + .not_null() + .default("{}"), + ) + // OAuth-specific fields + .col(ColumnDef::new(UserPlugins::OauthAccessToken).binary()) + .col(ColumnDef::new(UserPlugins::OauthRefreshToken).binary()) + .col(ColumnDef::new(UserPlugins::OauthExpiresAt).timestamp_with_time_zone()) + .col(ColumnDef::new(UserPlugins::OauthScope).text()) + // External user identity (for display) + .col(ColumnDef::new(UserPlugins::ExternalUserId).text()) + .col(ColumnDef::new(UserPlugins::ExternalUsername).text()) + .col(ColumnDef::new(UserPlugins::ExternalAvatarUrl).text()) + // Per-user state + .col( + ColumnDef::new(UserPlugins::Enabled) + .boolean() + .not_null() + .default(true), + ) + .col( + ColumnDef::new(UserPlugins::HealthStatus) + .string_len(20) + .not_null() + .default("unknown"), + ) + .col( + ColumnDef::new(UserPlugins::FailureCount) + .integer() + .not_null() + .default(0), + ) + .col(ColumnDef::new(UserPlugins::LastFailureAt).timestamp_with_time_zone()) + .col(ColumnDef::new(UserPlugins::LastSuccessAt).timestamp_with_time_zone()) + .col(ColumnDef::new(UserPlugins::LastSyncAt).timestamp_with_time_zone()) + // Timestamps + .col( + ColumnDef::new(UserPlugins::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(UserPlugins::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + // Foreign keys + .foreign_key( + ForeignKey::create() + .name("fk_user_plugins_plugin") + .from(UserPlugins::Table, UserPlugins::PluginId) + .to(Plugins::Table, Plugins::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_user_plugins_user") + .from(UserPlugins::Table, UserPlugins::UserId) + .to(Users::Table, Users::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Unique constraint: one instance per user per plugin + manager + .create_index( + Index::create() + .name("idx_user_plugins_plugin_user") + .table(UserPlugins::Table) + .col(UserPlugins::PluginId) + .col(UserPlugins::UserId) + .unique() + .to_owned(), + ) + .await?; + + // Index on user_id for fast lookups + manager + .create_index( + Index::create() + .name("idx_user_plugins_user_id") + .table(UserPlugins::Table) + .col(UserPlugins::UserId) + .to_owned(), + ) + .await?; + + // Index on plugin_id for broadcast operations + manager + .create_index( + Index::create() + .name("idx_user_plugins_plugin_id") + .table(UserPlugins::Table) + .col(UserPlugins::PluginId) + .to_owned(), + ) + .await?; + + // Index on enabled for filtering active instances + manager + .create_index( + Index::create() + .name("idx_user_plugins_enabled") + .table(UserPlugins::Table) + .col(UserPlugins::Enabled) + .to_owned(), + ) + .await?; + + // Create user_plugin_data table - key-value store per user-plugin instance + manager + .create_table( + Table::create() + .table(UserPluginData::Table) + .if_not_exists() + .col( + ColumnDef::new(UserPluginData::Id) + .uuid() + .not_null() + .primary_key(), + ) + // Reference to user's plugin instance + .col( + ColumnDef::new(UserPluginData::UserPluginId) + .uuid() + .not_null(), + ) + // Key-value storage + .col(ColumnDef::new(UserPluginData::Key).text().not_null()) + .col(ColumnDef::new(UserPluginData::Data).json().not_null()) + // Optional TTL for cached data + .col(ColumnDef::new(UserPluginData::ExpiresAt).timestamp_with_time_zone()) + // Timestamps + .col( + ColumnDef::new(UserPluginData::CreatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + .col( + ColumnDef::new(UserPluginData::UpdatedAt) + .timestamp_with_time_zone() + .not_null(), + ) + // Foreign key + .foreign_key( + ForeignKey::create() + .name("fk_user_plugin_data_user_plugin") + .from(UserPluginData::Table, UserPluginData::UserPluginId) + .to(UserPlugins::Table, UserPlugins::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Unique constraint: one value per key per user plugin instance + manager + .create_index( + Index::create() + .name("idx_user_plugin_data_user_plugin_key") + .table(UserPluginData::Table) + .col(UserPluginData::UserPluginId) + .col(UserPluginData::Key) + .unique() + .to_owned(), + ) + .await?; + + // Index on user_plugin_id for fast lookups + manager + .create_index( + Index::create() + .name("idx_user_plugin_data_user_plugin_id") + .table(UserPluginData::Table) + .col(UserPluginData::UserPluginId) + .to_owned(), + ) + .await?; + + // Partial index on expires_at for cleanup of expired data + manager + .create_index( + Index::create() + .name("idx_user_plugin_data_expires_at") + .table(UserPluginData::Table) + .col(UserPluginData::ExpiresAt) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Drop user_plugin_data first (depends on user_plugins) + manager + .drop_table(Table::drop().table(UserPluginData::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(UserPlugins::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +pub enum UserPlugins { + Table, + Id, + PluginId, + UserId, + Credentials, + Config, + OauthAccessToken, + OauthRefreshToken, + OauthExpiresAt, + OauthScope, + ExternalUserId, + ExternalUsername, + ExternalAvatarUrl, + Enabled, + HealthStatus, + FailureCount, + LastFailureAt, + LastSuccessAt, + LastSyncAt, + CreatedAt, + UpdatedAt, +} + +#[derive(DeriveIden)] +pub enum UserPluginData { + Table, + Id, + UserPluginId, + Key, + Data, + ExpiresAt, + CreatedAt, + UpdatedAt, +} + +// Local iden references for foreign keys +#[derive(DeriveIden)] +enum Plugins { + Table, + Id, +} + +#[derive(DeriveIden)] +enum Users { + Table, + Id, +} diff --git a/plugins/metadata-echo/package-lock.json b/plugins/metadata-echo/package-lock.json index fb4849f8..5a25373d 100644 --- a/plugins/metadata-echo/package-lock.json +++ b/plugins/metadata-echo/package-lock.json @@ -27,7 +27,7 @@ }, "../sdk-typescript": { "name": "@ashdev/codex-plugin-sdk", - "version": "1.8.5", + "version": "1.9.3", "license": "MIT", "devDependencies": { "@biomejs/biome": "^2.3.11", @@ -39,8 +39,14 @@ "node": ">=22.0.0" } }, - "../sdk-typescript/node_modules/@biomejs/biome": { - "version": "2.3.11", + "node_modules/@ashdev/codex-plugin-sdk": { + "resolved": "../sdk-typescript", + "link": true + }, + "node_modules/@biomejs/biome": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -54,18 +60,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.11", - "@biomejs/cli-darwin-x64": "2.3.11", - "@biomejs/cli-linux-arm64": "2.3.11", - "@biomejs/cli-linux-arm64-musl": "2.3.11", - "@biomejs/cli-linux-x64": "2.3.11", - "@biomejs/cli-linux-x64-musl": "2.3.11", - "@biomejs/cli-win32-arm64": "2.3.11", - "@biomejs/cli-win32-x64": "2.3.11" + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" } }, - "../sdk-typescript/node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.11", + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", "cpu": [ "arm64" ], @@ -79,1442 +87,2064 @@ "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } }, - "../sdk-typescript/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "darwin" - ] + "linux" + ], + "engines": { + "node": ">=14.21.3" + } }, - "../sdk-typescript/node_modules/@types/chai": { - "version": "5.2.3", + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@types/deep-eql": { - "version": "4.0.2", + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } }, - "../sdk-typescript/node_modules/@types/estree": { - "version": "1.0.8", + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } }, - "../sdk-typescript/node_modules/@types/node": { - "version": "22.19.7", + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@vitest/expect": { - "version": "3.2.4", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/mocker": { - "version": "3.2.4", + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/pretty-format": { - "version": "3.2.4", + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/runner": { - "version": "3.2.4", + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/snapshot": { - "version": "3.2.4", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/spy": { - "version": "3.2.4", + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/utils": { - "version": "3.2.4", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/assertion-error": { - "version": "2.0.1", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "../sdk-typescript/node_modules/cac": { - "version": "6.7.14", + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "../sdk-typescript/node_modules/chai": { - "version": "5.3.3", + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" } }, - "../sdk-typescript/node_modules/check-error": { - "version": "2.1.3", + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 16" + "node": ">=18" } }, - "../sdk-typescript/node_modules/debug": { - "version": "4.4.3", + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, - "../sdk-typescript/node_modules/deep-eql": { - "version": "5.0.2", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "../sdk-typescript/node_modules/es-module-lexer": { - "version": "1.7.0", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/esbuild": { - "version": "0.27.2", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "../sdk-typescript/node_modules/estree-walker": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" } }, - "../sdk-typescript/node_modules/expect-type": { - "version": "1.3.0", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/fdir": { - "version": "6.5.0", + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=18" } }, - "../sdk-typescript/node_modules/fsevents": { - "version": "2.3.3", + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/js-tokens": { - "version": "9.0.1", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/loupe": { - "version": "3.2.1", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/magic-string": { - "version": "0.30.21", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/nanoid": { - "version": "3.3.11", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" ], + "dev": true, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" } }, - "../sdk-typescript/node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/pathval": { - "version": "2.0.1", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 14.16" + "node": ">=18" } }, - "../sdk-typescript/node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "../sdk-typescript/node_modules/picomatch": { - "version": "4.0.3", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "../sdk-typescript/node_modules/postcss": { - "version": "8.5.6", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" } }, - "../sdk-typescript/node_modules/rollup": { - "version": "4.57.0", + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", - "fsevents": "~2.3.2" + "node": ">=18" } }, - "../sdk-typescript/node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../sdk-typescript/node_modules/source-map-js": { - "version": "1.2.1", + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/std-env": { - "version": "3.10.0", + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "../sdk-typescript/node_modules/strip-literal": { - "version": "3.1.0", + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/tinybench": { - "version": "2.9.0", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, - "../sdk-typescript/node_modules/tinyexec": { - "version": "0.3.2", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "../sdk-typescript/node_modules/tinyglobby": { - "version": "0.2.15", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" } }, - "../sdk-typescript/node_modules/tinypool": { - "version": "1.1.1", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "../sdk-typescript/node_modules/tinyrainbow": { - "version": "2.0.0", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "../sdk-typescript/node_modules/tinyspy": { - "version": "4.0.4", + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "../sdk-typescript/node_modules/typescript": { - "version": "5.9.3", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=14.17" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "../sdk-typescript/node_modules/undici-types": { - "version": "6.21.0", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "../sdk-typescript/node_modules/vite": { - "version": "7.3.1", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">= 14.16" } }, - "../sdk-typescript/node_modules/vite-node": { - "version": "3.2.4", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "../sdk-typescript/node_modules/vitest": { - "version": "3.2.4", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, - "happy-dom": { - "optional": true + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" }, - "jsdom": { - "optional": true + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - } - }, - "../sdk-typescript/node_modules/why-is-node-running": { - "version": "2.3.0", - "dev": true, + ], "license": "MIT", "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/@ashdev/codex-plugin-sdk": { - "resolved": "../sdk-typescript", - "link": true - }, - "node_modules/@biomejs/biome": { - "version": "2.3.13", + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, - "license": "MIT OR Apache-2.0", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, "bin": { - "biome": "bin/biome" + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.13", - "@biomejs/cli-darwin-x64": "2.3.13", - "@biomejs/cli-linux-arm64": "2.3.13", - "@biomejs/cli-linux-arm64-musl": "2.3.13", - "@biomejs/cli-linux-x64": "2.3.13", - "@biomejs/cli-linux-x64-musl": "2.3.13", - "@biomejs/cli-win32-arm64": "2.3.13", - "@biomejs/cli-win32-x64": "2.3.13" + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" } }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.13", - "cpu": [ - "arm64" - ], + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } + "license": "ISC" }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "cpu": [ - "arm64" - ], + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "BSD-3-Clause", "engines": { - "node": ">=18" + "node": ">=0.10.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "cpu": [ - "arm64" - ], + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "MIT" }, - "node_modules/@types/chai": { - "version": "5.2.3", + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.8", + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.19.7", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "engines": { + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@vitest/runner": { - "version": "3.2.4", + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=14.17" } }, - "node_modules/@vitest/spy": { - "version": "3.2.4", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "MIT" }, - "node_modules/@vitest/utils": { - "version": "3.2.4", + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/chai": { - "version": "5.3.3", + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=18" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/check-error": { - "version": "2.1.3", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 16" + "node": ">=18" } }, - "node_modules/debug": { - "version": "4.4.3", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, - "node_modules/deep-eql": { - "version": "5.0.2", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.24.2", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" } }, - "node_modules/estree-walker": { - "version": "3.0.3", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/fdir": { - "version": "6.5.0", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=18" } }, - "node_modules/fsevents": { - "version": "2.3.3", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "freebsd" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "optional": true, + "os": [ + "freebsd" ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" } }, - "node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 14.16" + "node": ">=18" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "node_modules/postcss": { - "version": "8.5.6", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" ], + "dev": true, "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" } }, - "node_modules/rollup": { - "version": "4.57.0", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", - "fsevents": "~2.3.2" + "node": ">=18" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-literal": { - "version": "3.1.0", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tinybench": { - "version": "2.9.0", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tinyexec": { - "version": "0.3.2", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tinyglobby": { - "version": "0.2.15", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tinypool": { - "version": "1.1.1", + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "2.0.0", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/tinyspy": { - "version": "4.0.4", + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/typescript": { - "version": "5.9.3", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14.17" + "node": ">=18" } }, - "node_modules/undici-types": { - "version": "6.21.0", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/vite": { - "version": "7.3.1", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">=18" } }, - "node_modules/vite-node": { - "version": "3.2.4", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=18" } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.27.2", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1525,36 +2155,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/vitest": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { @@ -1626,6 +2258,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/plugins/metadata-echo/src/index.ts b/plugins/metadata-echo/src/index.ts index 38873bca..6d3d4771 100644 --- a/plugins/metadata-echo/src/index.ts +++ b/plugins/metadata-echo/src/index.ts @@ -366,7 +366,7 @@ createMetadataPlugin({ logLevel: "debug", onInitialize(params: InitializeParams) { // Read config from initialization params - const maxResults = params.config?.maxResults as number | undefined; + const maxResults = params.adminConfig?.maxResults as number | undefined; if (maxResults !== undefined) { config.maxResults = Math.min(Math.max(1, maxResults), 20); // Clamp 1-20 } diff --git a/plugins/metadata-mangabaka/package-lock.json b/plugins/metadata-mangabaka/package-lock.json index 51eeb734..349d5a02 100644 --- a/plugins/metadata-mangabaka/package-lock.json +++ b/plugins/metadata-mangabaka/package-lock.json @@ -27,7 +27,7 @@ }, "../sdk-typescript": { "name": "@ashdev/codex-plugin-sdk", - "version": "1.8.5", + "version": "1.9.3", "license": "MIT", "devDependencies": { "@biomejs/biome": "^2.3.11", @@ -39,8 +39,14 @@ "node": ">=22.0.0" } }, - "../sdk-typescript/node_modules/@biomejs/biome": { - "version": "2.3.11", + "node_modules/@ashdev/codex-plugin-sdk": { + "resolved": "../sdk-typescript", + "link": true + }, + "node_modules/@biomejs/biome": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -54,18 +60,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.11", - "@biomejs/cli-darwin-x64": "2.3.11", - "@biomejs/cli-linux-arm64": "2.3.11", - "@biomejs/cli-linux-arm64-musl": "2.3.11", - "@biomejs/cli-linux-x64": "2.3.11", - "@biomejs/cli-linux-x64-musl": "2.3.11", - "@biomejs/cli-win32-arm64": "2.3.11", - "@biomejs/cli-win32-x64": "2.3.11" + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" } }, - "../sdk-typescript/node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.11", + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", "cpu": [ "arm64" ], @@ -79,1205 +87,1710 @@ "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", "cpu": [ - "arm64" + "x64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "license": "MIT OR Apache-2.0", "optional": true, "os": [ - "darwin" - ] - }, - "../sdk-typescript/node_modules/@types/chai": { - "version": "5.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@types/deep-eql": { - "version": "4.0.2", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/@types/estree": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/@types/node": { - "version": "22.19.7", + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@vitest/expect": { - "version": "3.2.4", + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@vitest/mocker": { - "version": "3.2.4", + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@vitest/pretty-format": { - "version": "3.2.4", + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@vitest/runner": { - "version": "3.2.4", + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" } }, - "../sdk-typescript/node_modules/@vitest/snapshot": { - "version": "3.2.4", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/spy": { - "version": "3.2.4", + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/@vitest/utils": { - "version": "3.2.4", + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/assertion-error": { - "version": "2.0.1", + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "../sdk-typescript/node_modules/cac": { - "version": "6.7.14", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "../sdk-typescript/node_modules/chai": { - "version": "5.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=18" } }, - "../sdk-typescript/node_modules/check-error": { - "version": "2.1.3", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 16" + "node": ">=18" } }, - "../sdk-typescript/node_modules/debug": { - "version": "4.4.3", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, - "../sdk-typescript/node_modules/deep-eql": { - "version": "5.0.2", + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "../sdk-typescript/node_modules/es-module-lexer": { - "version": "1.7.0", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/esbuild": { - "version": "0.27.2", + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, - "../sdk-typescript/node_modules/estree-walker": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" } }, - "../sdk-typescript/node_modules/expect-type": { - "version": "1.3.0", + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/fdir": { - "version": "6.5.0", + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=18" } }, - "../sdk-typescript/node_modules/fsevents": { - "version": "2.3.3", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/js-tokens": { - "version": "9.0.1", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/loupe": { - "version": "3.2.1", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/magic-string": { - "version": "0.30.21", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/nanoid": { - "version": "3.3.11", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" ], + "dev": true, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" } }, - "../sdk-typescript/node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/pathval": { - "version": "2.0.1", + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 14.16" + "node": ">=18" } }, - "../sdk-typescript/node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "../sdk-typescript/node_modules/picomatch": { - "version": "4.0.3", + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "../sdk-typescript/node_modules/postcss": { - "version": "8.5.6", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" } }, - "../sdk-typescript/node_modules/rollup": { - "version": "4.57.0", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", - "fsevents": "~2.3.2" + "node": ">=18" } }, - "../sdk-typescript/node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../sdk-typescript/node_modules/source-map-js": { - "version": "1.2.1", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/std-env": { - "version": "3.10.0", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "../sdk-typescript/node_modules/strip-literal": { - "version": "3.1.0", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, - "../sdk-typescript/node_modules/tinybench": { - "version": "2.9.0", + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "../sdk-typescript/node_modules/tinyexec": { - "version": "0.3.2", + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "../sdk-typescript/node_modules/tinyglobby": { - "version": "0.2.15", + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "../sdk-typescript/node_modules/tinypool": { - "version": "1.1.1", + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "../sdk-typescript/node_modules/tinyrainbow": { - "version": "2.0.0", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "../sdk-typescript/node_modules/tinyspy": { - "version": "4.0.4", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "undici-types": "~6.21.0" } }, - "../sdk-typescript/node_modules/typescript": { - "version": "5.9.3", + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, - "engines": { - "node": ">=14.17" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "../sdk-typescript/node_modules/undici-types": { - "version": "6.21.0", - "dev": true, - "license": "MIT" - }, - "../sdk-typescript/node_modules/vite": { - "version": "7.3.1", + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { + "msw": { "optional": true }, - "yaml": { + "vite": { "optional": true } } }, - "../sdk-typescript/node_modules/vite-node": { + "node_modules/@vitest/pretty-format": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "../sdk-typescript/node_modules/vitest": { + "node_modules/@vitest/runner": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } } }, - "../sdk-typescript/node_modules/why-is-node-running": { - "version": "2.3.0", + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@ashdev/codex-plugin-sdk": { - "resolved": "../sdk-typescript", - "link": true - }, - "node_modules/@biomejs/biome": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", - "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.14", - "@biomejs/cli-darwin-x64": "2.3.14", - "@biomejs/cli-linux-arm64": "2.3.14", - "@biomejs/cli-linux-arm64-musl": "2.3.14", - "@biomejs/cli-linux-x64": "2.3.14", - "@biomejs/cli-linux-x64-musl": "2.3.14", - "@biomejs/cli-win32-arm64": "2.3.14", - "@biomejs/cli-win32-x64": "2.3.14" + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", - "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", - "cpu": [ - "arm64" - ], + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, "engines": { - "node": ">=14.21.3" + "node": ">=18" } }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", - "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", - "cpu": [ - "x64" - ], + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=14.21.3" + "node": ">= 16" } }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", - "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", - "cpu": [ - "arm64" - ], + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=14.21.3" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", - "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", - "cpu": [ - "arm64" - ], + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=14.21.3" + "node": ">=6" } }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", - "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", - "cpu": [ - "x64" - ], + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=14.21.3" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", - "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", - "cpu": [ - "x64" - ], + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", - "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", - "cpu": [ - "arm64" - ], + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", "engines": { - "node": ">=14.21.3" + "node": ">=12.0.0" } }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.14", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", - "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", - "cpu": [ - "x64" - ], + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">=14.21.3" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "cpu": [ - "arm64" - ], + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "cpu": [ - "arm64" - ], + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "MIT" }, - "node_modules/@types/chai": { - "version": "5.2.3", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.8", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.19.7", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "engines": { + "node": ">= 14.16" } }, - "node_modules/@vitest/expect": { - "version": "3.2.4", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "engines": { + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "@types/estree": "1.0.8" }, - "funding": { - "url": "https://opencollective.com/vitest" + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" } }, - "node_modules/@vitest/runner": { - "version": "3.2.4", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" + "js-tokens": "^9.0.1" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@vitest/spy": { - "version": "3.2.4", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^4.0.3" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/@vitest/utils": { - "version": "3.2.4", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/assertion-error": { - "version": "2.0.1", + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/cac": { - "version": "6.7.14", + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/chai": { - "version": "5.3.3", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/check-error": { - "version": "2.1.3", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">= 16" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/debug": { - "version": "4.4.3", + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=6.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/deep-eql": { - "version": "5.0.2", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.24.2", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" } }, - "node_modules/estree-walker": { - "version": "3.0.3", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/fdir": { - "version": "6.5.0", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=18" } }, - "node_modules/fsevents": { - "version": "2.3.3", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, @@ -1285,359 +1798,353 @@ "darwin" ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=18" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "optional": true, + "os": [ + "darwin" ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=18" } }, - "node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 14.16" + "node": ">=18" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "node_modules/postcss": { - "version": "8.5.6", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" } }, - "node_modules/rollup": { - "version": "4.57.0", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", - "fsevents": "~2.3.2" + "node": ">=18" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/stackback": { - "version": "0.0.2", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/std-env": { - "version": "3.10.0", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/strip-literal": { - "version": "3.1.0", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tinybench": { - "version": "2.9.0", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tinyexec": { - "version": "0.3.2", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tinyglobby": { - "version": "0.2.15", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tinypool": { - "version": "1.1.1", + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "2.0.0", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/tinyspy": { - "version": "4.0.4", + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/typescript": { - "version": "5.9.3", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14.17" + "node": ">=18" } }, - "node_modules/undici-types": { - "version": "6.21.0", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/vite": { - "version": "7.3.1", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "node": ">=18" } }, - "node_modules/vite-node": { - "version": "3.2.4", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=18" } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.27.2", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1648,36 +2155,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/vitest": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { @@ -1749,6 +2258,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/plugins/metadata-mangabaka/src/handlers/match.test.ts b/plugins/metadata-mangabaka/src/handlers/match.test.ts new file mode 100644 index 00000000..ec6bced9 --- /dev/null +++ b/plugins/metadata-mangabaka/src/handlers/match.test.ts @@ -0,0 +1,158 @@ +import type { MetadataMatchParams, SearchResult } from "@ashdev/codex-plugin-sdk"; +import { describe, expect, it } from "vitest"; +import { scoreResult, similarity } from "./match.js"; + +function makeResult(overrides: Partial): SearchResult { + return { + externalId: "1", + title: "Test", + alternateTitles: [], + ...overrides, + }; +} + +describe("similarity", () => { + it("should return 1.0 for exact case-insensitive match", () => { + expect(similarity("Air", "Air")).toBe(1.0); + expect(similarity("Air", "air")).toBe(1.0); + expect(similarity("DRAGON BALL", "dragon ball")).toBe(1.0); + }); + + it("should return 0 when one string is empty", () => { + expect(similarity("", "Air")).toBe(0); + expect(similarity("Air", "")).toBe(0); + }); + + it("should return 1.0 when both strings are empty (identical)", () => { + expect(similarity("", "")).toBe(1.0); + }); + + it("should penalize containment by length ratio", () => { + // "Air" (3 chars) in "Air Gear" (8 chars): containment = 0.8 * 3/8 = 0.3 + // Jaccard: {"air"} vs {"air", "gear"} = 1/2 = 0.5 + const score = similarity("Air", "Air Gear"); + expect(score).toBeCloseTo(0.5, 2); + expect(score).toBeLessThan(0.8); + }); + + it("should give reasonable score for similar-length containment", () => { + // "Dragon Ball" (11 chars) in "Dragon Ball Z" (13 chars): containment = 0.8 * 11/13 ≈ 0.677 + // Jaccard: {"dragon", "ball"} vs {"dragon", "ball", "z"} = 2/3 ≈ 0.667 + const score = similarity("Dragon Ball", "Dragon Ball Z"); + expect(score).toBeGreaterThan(0.6); + expect(score).toBeLessThan(0.8); + }); + + it("should return 0 for completely different strings", () => { + expect(similarity("Air", "Naruto")).toBe(0); + expect(similarity("One Piece", "Bleach")).toBe(0); + }); + + it("should handle single-word containment with length penalty", () => { + // "Air" (3 chars) in "Airing" (6 chars): containment = 0.8 * 3/6 = 0.4 + // Jaccard: {"air"} vs {"airing"} = 0/2 = 0 (different words) + const score = similarity("Air", "Airing"); + expect(score).toBeCloseTo(0.4, 2); + }); + + it("should use Jaccard when it produces higher score than containment", () => { + // "Air" in "Air Gear": containment = 0.3, Jaccard = 0.5 -> Jaccard wins + const score = similarity("Air", "Air Gear"); + expect(score).toBeCloseTo(0.5, 2); + }); + + it("should handle word overlap without containment", () => { + // "One Piece" vs "Piece of Cake" + // No containment (neither contains the other) + // Jaccard: {"one", "piece"} vs {"piece", "of", "cake"} = 1/4 = 0.25 + const score = similarity("One Piece", "Piece of Cake"); + expect(score).toBeCloseTo(0.25, 2); + }); + + it("should be symmetric", () => { + expect(similarity("Air", "Air Gear")).toBe(similarity("Air Gear", "Air")); + expect(similarity("Naruto", "Naruto Shippuden")).toBe(similarity("Naruto Shippuden", "Naruto")); + }); +}); + +describe("scoreResult", () => { + it("should score exact title match at 0.8 without year", () => { + const result = makeResult({ title: "Air" }); + const params: MetadataMatchParams = { title: "Air" }; + expect(scoreResult(result, params)).toBeCloseTo(0.8, 2); + }); + + it("should score exact title match at 1.0 with matching year", () => { + const result = makeResult({ title: "Air", year: 2005 }); + const params: MetadataMatchParams = { title: "Air", year: 2005 }; + expect(scoreResult(result, params)).toBeCloseTo(1.0, 2); + }); + + it("should score containment match significantly lower than exact", () => { + const air = makeResult({ title: "Air" }); + const airGear = makeResult({ title: "Air Gear" }); + const params: MetadataMatchParams = { title: "Air" }; + + const airScore = scoreResult(air, params); + const airGearScore = scoreResult(airGear, params); + + expect(airScore).toBeGreaterThan(airGearScore + 0.2); + }); + + it("should prefer 'Air' over 'Air Gear' when searching for 'Air'", () => { + const air = makeResult({ title: "Air", externalId: "air" }); + const airGear = makeResult({ title: "Air Gear", externalId: "air-gear" }); + const params: MetadataMatchParams = { title: "Air" }; + + expect(scoreResult(air, params)).toBe(0.8); + expect(scoreResult(airGear, params)).toBe(0.3); + }); + + it("should check alternate titles for best similarity", () => { + const result = makeResult({ + title: "AIR (TV)", + alternateTitles: ["Air", "エアー"], + }); + const params: MetadataMatchParams = { title: "Air" }; + // Alternate title "Air" is an exact match -> similarity 1.0 + // Score = 1.0 * 0.6 + 0.2 (exact bonus) = 0.8 + expect(scoreResult(result, params)).toBeCloseTo(0.8, 2); + }); + + it("should give exact match bonus when only alternate title matches", () => { + const result = makeResult({ + title: "Completely Different Title", + alternateTitles: ["Air"], + }); + const params: MetadataMatchParams = { title: "Air" }; + // bestSimilarity from "Air" alt = 1.0 + // Score = 1.0 * 0.6 + 0.2 (exact alt match) = 0.8 + expect(scoreResult(result, params)).toBeCloseTo(0.8, 2); + }); + + it("should give partial year credit for year off by 1", () => { + const result = makeResult({ title: "Air", year: 2006 }); + const params: MetadataMatchParams = { title: "Air", year: 2005 }; + // 1.0 * 0.6 + 0.1 (year ±1) + 0.2 (exact) = 0.9 + expect(scoreResult(result, params)).toBeCloseTo(0.9, 2); + }); + + it("should give no year credit when years differ by more than 1", () => { + const result = makeResult({ title: "Air", year: 2010 }); + const params: MetadataMatchParams = { title: "Air", year: 2005 }; + // 1.0 * 0.6 + 0 (year too far) + 0.2 (exact) = 0.8 + expect(scoreResult(result, params)).toBeCloseTo(0.8, 2); + }); + + it("should not give year credit when year is missing from result", () => { + const result = makeResult({ title: "Air" }); + const params: MetadataMatchParams = { title: "Air", year: 2005 }; + expect(scoreResult(result, params)).toBeCloseTo(0.8, 2); + }); + + it("should cap score at 1.0", () => { + const result = makeResult({ title: "Air", year: 2005 }); + const params: MetadataMatchParams = { title: "Air", year: 2005 }; + expect(scoreResult(result, params)).toBeLessThanOrEqual(1.0); + }); +}); diff --git a/plugins/metadata-mangabaka/src/handlers/match.ts b/plugins/metadata-mangabaka/src/handlers/match.ts index 09983c21..c9b43e79 100644 --- a/plugins/metadata-mangabaka/src/handlers/match.ts +++ b/plugins/metadata-mangabaka/src/handlers/match.ts @@ -10,40 +10,56 @@ import { mapSearchResult } from "../mappers.js"; const logger = createLogger({ name: "mangabaka-match", level: "info" }); /** - * Calculate string similarity using word overlap + * Calculate string similarity using word overlap and containment scoring * Returns a value between 0 and 1 */ -function similarity(a: string, b: string): number { +export function similarity(a: string, b: string): number { const aLower = a.toLowerCase().trim(); const bLower = b.toLowerCase().trim(); if (aLower === bLower) return 1.0; if (aLower.length === 0 || bLower.length === 0) return 0; - // Check if one contains the other - if (aLower.includes(bLower) || bLower.includes(aLower)) { - return 0.8; + let score = 0; + + // Containment check with length-ratio penalty + // Prevents short queries like "Air" from matching "Air Gear" too strongly + const shorter = aLower.length <= bLower.length ? aLower : bLower; + const longer = aLower.length <= bLower.length ? bLower : aLower; + + if (longer.includes(shorter)) { + const lengthRatio = shorter.length / longer.length; + score = Math.max(score, 0.8 * lengthRatio); } - // Simple word overlap scoring + // Word overlap scoring (Jaccard similarity) const aWords = new Set(aLower.split(/\s+/)); const bWords = new Set(bLower.split(/\s+/)); const intersection = [...aWords].filter((w) => bWords.has(w)); const union = new Set([...aWords, ...bWords]); - return intersection.length / union.size; + if (union.size > 0) { + score = Math.max(score, intersection.length / union.size); + } + + return score; } /** * Score a search result against the match parameters * Returns a value between 0 and 1 */ -function scoreResult(result: SearchResult, params: MetadataMatchParams): number { +export function scoreResult(result: SearchResult, params: MetadataMatchParams): number { let score = 0; + // Find best title similarity across primary and alternate titles + let bestTitleSimilarity = similarity(result.title, params.title); + for (const alt of result.alternateTitles) { + bestTitleSimilarity = Math.max(bestTitleSimilarity, similarity(alt, params.title)); + } + // Title similarity (up to 0.6) - const titleScore = similarity(result.title, params.title); - score += titleScore * 0.6; + score += bestTitleSimilarity * 0.6; // Year match (up to 0.2) if (params.year && result.year) { @@ -54,8 +70,13 @@ function scoreResult(result: SearchResult, params: MetadataMatchParams): number } } - // Boost for exact title match (up to 0.2) - if (result.title.toLowerCase() === params.title.toLowerCase()) { + // Boost for exact title match across primary and alternate titles (up to 0.2) + const searchLower = params.title.toLowerCase(); + const hasExactMatch = + result.title.toLowerCase() === searchLower || + result.alternateTitles.some((alt) => alt.toLowerCase() === searchLower); + + if (hasExactMatch) { score += 0.2; } diff --git a/plugins/metadata-mangabaka/src/index.ts b/plugins/metadata-mangabaka/src/index.ts index 5e0bcc27..3a171371 100644 --- a/plugins/metadata-mangabaka/src/index.ts +++ b/plugins/metadata-mangabaka/src/index.ts @@ -62,7 +62,7 @@ createMetadataPlugin({ } // Get optional timeout from config (in seconds) - const timeout = params.config?.timeout as number | undefined; + const timeout = params.adminConfig?.timeout as number | undefined; client = new MangaBakaClient(apiKey, { timeout }); logger.info(`MangaBaka client initialized (timeout: ${timeout ?? "default"}s)`); diff --git a/plugins/metadata-mangabaka/src/mappers.ts b/plugins/metadata-mangabaka/src/mappers.ts index 7b4cfa60..afa73d67 100644 --- a/plugins/metadata-mangabaka/src/mappers.ts +++ b/plugins/metadata-mangabaka/src/mappers.ts @@ -4,6 +4,7 @@ import type { AlternateTitle, + ExternalId, ExternalLink, ExternalRating, PluginSeriesMetadata, @@ -268,8 +269,12 @@ export function mapSeriesMetadata(series: MbSeries): PluginSeriesMetadata { }, }; - // Build external links and ratings from sources in a single pass + // Build external links, ratings, and cross-reference IDs from sources in a single pass const externalRatings: ExternalRating[] = []; + const externalIds: ExternalId[] = [ + // Always include the plugin's own API ID so other plugins can cross-reference + { source: "api:mangabaka", externalId: String(series.id) }, + ]; if (series.source) { for (const [key, info] of Object.entries(series.source)) { @@ -279,6 +284,14 @@ export function mapSeriesMetadata(series: MbSeries): PluginSeriesMetadata { // Use config if available, otherwise generate defaults from key const ratingKey = config?.ratingKey ?? key.replace(/_/g, ""); + // Add cross-reference external ID with api: prefix + if (info.id != null) { + externalIds.push({ + source: `api:${ratingKey}`, + externalId: String(info.id), + }); + } + // Add external link if source has an ID and URL pattern if (info.id != null && config?.urlPattern) { externalLinks.push({ @@ -323,5 +336,6 @@ export function mapSeriesMetadata(series: MbSeries): PluginSeriesMetadata { })(), externalRatings: externalRatings.length > 0 ? externalRatings : undefined, externalLinks, + externalIds, }; } diff --git a/plugins/metadata-openlibrary/package-lock.json b/plugins/metadata-openlibrary/package-lock.json index e394795c..d827b325 100644 --- a/plugins/metadata-openlibrary/package-lock.json +++ b/plugins/metadata-openlibrary/package-lock.json @@ -27,7 +27,7 @@ }, "../sdk-typescript": { "name": "@ashdev/codex-plugin-sdk", - "version": "1.8.5", + "version": "1.9.3", "license": "MIT", "devDependencies": { "@biomejs/biome": "^2.3.11", @@ -44,9 +44,9 @@ "link": true }, "node_modules/@biomejs/biome": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz", - "integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -60,20 +60,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.13", - "@biomejs/cli-darwin-x64": "2.3.13", - "@biomejs/cli-linux-arm64": "2.3.13", - "@biomejs/cli-linux-arm64-musl": "2.3.13", - "@biomejs/cli-linux-x64": "2.3.13", - "@biomejs/cli-linux-x64-musl": "2.3.13", - "@biomejs/cli-win32-arm64": "2.3.13", - "@biomejs/cli-win32-x64": "2.3.13" + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz", - "integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", "cpu": [ "arm64" ], @@ -88,9 +88,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz", - "integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", "cpu": [ "x64" ], @@ -105,9 +105,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz", - "integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", "cpu": [ "arm64" ], @@ -122,9 +122,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz", - "integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", "cpu": [ "arm64" ], @@ -139,9 +139,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz", - "integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", "cpu": [ "x64" ], @@ -156,9 +156,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz", - "integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", "cpu": [ "x64" ], @@ -173,9 +173,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz", - "integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", "cpu": [ "arm64" ], @@ -190,9 +190,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz", - "integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", "cpu": [ "x64" ], @@ -564,9 +564,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -1031,9 +1031,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", - "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", "dev": true, "license": "MIT", "dependencies": { @@ -1717,9 +1717,9 @@ } }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -1734,9 +1734,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -1751,9 +1751,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -1768,9 +1768,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -1785,9 +1785,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -1802,9 +1802,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -1819,9 +1819,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -1836,9 +1836,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -1853,9 +1853,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -1870,9 +1870,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -1887,9 +1887,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -1904,9 +1904,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -1921,9 +1921,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -1938,9 +1938,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -1955,9 +1955,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -1972,9 +1972,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -1989,9 +1989,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -2006,9 +2006,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -2023,9 +2023,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -2040,9 +2040,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -2057,9 +2057,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -2074,9 +2074,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -2091,9 +2091,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -2108,9 +2108,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -2125,9 +2125,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -2142,9 +2142,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2155,32 +2155,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/vitest": { diff --git a/plugins/metadata-openlibrary/src/index.ts b/plugins/metadata-openlibrary/src/index.ts index 98c8e147..0946bb42 100644 --- a/plugins/metadata-openlibrary/src/index.ts +++ b/plugins/metadata-openlibrary/src/index.ts @@ -248,7 +248,7 @@ createMetadataPlugin({ logLevel: "info", onInitialize(params: InitializeParams) { // Read config from initialization params - const maxResults = params.config?.maxResults as number | undefined; + const maxResults = params.adminConfig?.maxResults as number | undefined; if (maxResults !== undefined) { config.maxResults = Math.min(Math.max(1, maxResults), 50); // Clamp 1-50 } diff --git a/plugins/recommendations-anilist/README.md b/plugins/recommendations-anilist/README.md new file mode 100644 index 00000000..c8bd5c5c --- /dev/null +++ b/plugins/recommendations-anilist/README.md @@ -0,0 +1,127 @@ +# @ashdev/codex-plugin-recommendations-anilist + +A Codex plugin for personalized manga recommendations powered by [AniList](https://anilist.co) community data. Generates recommendations based on your reading history and ratings. + +## Features + +- Personalized manga recommendations from AniList +- Based on your library ratings and reading history +- Configurable maximum number of recommendations +- Uses AniList's recommendation and user list APIs + +## Authentication + +This plugin supports two authentication methods: + +### OAuth (Recommended) + +If your Codex administrator has configured OAuth: + +1. Go to **Settings** > **Integrations** +2. Click **Connect with AniList Recommendations** +3. Authorize Codex on AniList +4. You're connected! + +### Personal Access Token + +If OAuth is not configured by the admin: + +1. Go to [AniList Developer Settings](https://anilist.co/settings/developer) +2. Click **Create New Client** +3. Set the redirect URL to `https://anilist.co/api/v2/oauth/pin` +4. Click **Save**, then **Authorize** your new client +5. Copy the token shown on the pin page +6. In Codex, go to **Settings** > **Integrations** +7. Paste the token in the access token field and click **Save Token** + +## Admin Setup + +### Adding the Plugin to Codex + +1. Log in to Codex as an administrator +2. Navigate to **Settings** > **Plugins** +3. Click **Add Plugin** +4. Fill in the form: + - **Name**: `recommendations-anilist` + - **Display Name**: `AniList Recommendations` + - **Command**: `npx` + - **Arguments**: `-y @ashdev/codex-plugin-recommendations-anilist@1.9.3` +5. Click **Save** +6. Click **Test Connection** to verify the plugin works + +### Configuring OAuth (Optional) + +To enable OAuth login for your users: + +1. Go to [AniList Developer Settings](https://anilist.co/settings/developer) +2. Click **Create New Client** +3. Set the redirect URL to `{your-codex-url}/api/v1/user/plugins/oauth/callback` +4. Save and copy the **Client ID** +5. In Codex, go to **Settings** > **Plugins** > click the gear icon on AniList Recommendations +6. Go to the **OAuth** tab +7. Paste the **Client ID** (and optionally the **Client Secret**) +8. Click **Save Changes** + +Without OAuth configured, users can still connect by pasting a personal access token. + +### npx Options + +| Configuration | Arguments | Description | +|--------------|-----------|-------------| +| Latest version | `-y @ashdev/codex-plugin-recommendations-anilist` | Always uses latest | +| Pinned version | `-y @ashdev/codex-plugin-recommendations-anilist@1.9.3` | Recommended for production | +| Fast startup | `-y --prefer-offline @ashdev/codex-plugin-recommendations-anilist@1.9.3` | Skips version check if cached | + +## Configuration + +### Plugin Config + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `maxRecommendations` | number | `20` | Maximum number of recommendations to generate (1-50) | + +## Using the Plugin + +Once connected, recommendations appear in the Codex UI: + +1. Go to **Settings** > **Integrations** and verify the plugin shows as **Connected** +2. Recommendations are generated based on your library ratings and reading history + +## Development + +```bash +# Install dependencies +npm install + +# Build the plugin +npm run build + +# Type check +npm run typecheck + +# Run tests +npm test + +# Lint +npm run lint +``` + +## Project Structure + +``` +plugins/recommendations-anilist/ +├── src/ +│ ├── index.ts # Plugin entry point +│ ├── manifest.ts # Plugin manifest +│ ├── anilist.ts # AniList API client +│ └── anilist.test.ts # API client tests +├── dist/ +│ └── index.js # Built bundle (excluded from git) +├── package.json +├── tsconfig.json +└── README.md +``` + +## License + +MIT diff --git a/plugins/recommendations-anilist/package-lock.json b/plugins/recommendations-anilist/package-lock.json new file mode 100644 index 00000000..9f226507 --- /dev/null +++ b/plugins/recommendations-anilist/package-lock.json @@ -0,0 +1,2277 @@ +{ + "name": "@ashdev/codex-plugin-recommendations-anilist", + "version": "1.9.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ashdev/codex-plugin-recommendations-anilist", + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "@ashdev/codex-plugin-sdk": "file:../sdk-typescript" + }, + "bin": { + "codex-plugin-recommendations-anilist": "dist/index.js" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.13", + "@types/node": "^22.0.0", + "esbuild": "^0.24.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "../sdk-typescript": { + "name": "@ashdev/codex-plugin-sdk", + "version": "1.9.3", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@ashdev/codex-plugin-sdk": { + "resolved": "../sdk-typescript", + "link": true + }, + "node_modules/@biomejs/biome": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/plugins/recommendations-anilist/package.json b/plugins/recommendations-anilist/package.json new file mode 100644 index 00000000..7e607713 --- /dev/null +++ b/plugins/recommendations-anilist/package.json @@ -0,0 +1,51 @@ +{ + "name": "@ashdev/codex-plugin-recommendations-anilist", + "version": "1.9.3", + "description": "AniList recommendation provider plugin for Codex - generates personalized manga recommendations based on your reading history", + "main": "dist/index.js", + "bin": "dist/index.js", + "type": "module", + "files": [ + "dist", + "README.md" + ], + "repository": { + "type": "git", + "url": "https://github.com/AshDevFr/codex.git", + "directory": "plugins/recommendations-anilist" + }, + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'", + "dev": "npm run build -- --watch", + "clean": "rm -rf dist", + "start": "node dist/index.js", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "test:watch": "vitest", + "prepublishOnly": "npm run lint && npm run build" + }, + "keywords": [ + "codex", + "plugin", + "anilist", + "recommendations", + "manga" + ], + "author": "Codex", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + }, + "dependencies": { + "@ashdev/codex-plugin-sdk": "file:../sdk-typescript" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.13", + "@types/node": "^22.0.0", + "esbuild": "^0.24.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/plugins/recommendations-anilist/src/anilist.test.ts b/plugins/recommendations-anilist/src/anilist.test.ts new file mode 100644 index 00000000..11454e30 --- /dev/null +++ b/plugins/recommendations-anilist/src/anilist.test.ts @@ -0,0 +1,242 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { AniListRecommendationClient, getBestTitle, stripHtml } from "./anilist.js"; + +describe("getBestTitle", () => { + it("prefers English title", () => { + expect(getBestTitle({ romaji: "Shingeki no Kyojin", english: "Attack on Titan" })).toBe( + "Attack on Titan", + ); + }); + + it("falls back to romaji", () => { + expect(getBestTitle({ romaji: "Berserk" })).toBe("Berserk"); + }); + + it("falls back to romaji when english is empty", () => { + expect(getBestTitle({ romaji: "Berserk", english: "" })).toBe("Berserk"); + }); + + it("returns Unknown when neither is set", () => { + expect(getBestTitle({})).toBe("Unknown"); + }); +}); + +describe("stripHtml", () => { + it("strips basic tags", () => { + expect(stripHtml("

Hello world

")).toBe("Hello world"); + }); + + it("converts br to newlines", () => { + expect(stripHtml("Line 1
Line 2
Line 3")).toBe("Line 1\nLine 2\nLine 3"); + }); + + it("returns undefined for null", () => { + expect(stripHtml(null)).toBeUndefined(); + }); + + it("returns undefined for empty string after trim", () => { + expect(stripHtml(" ")).toBe(""); + }); + + it("handles complex HTML", () => { + expect(stripHtml('A story about heroes')).toBe("A story about heroes"); + }); + + it("decodes named HTML entities", () => { + expect(stripHtml("Tom & Jerry")).toBe("Tom & Jerry"); + expect(stripHtml("a < b > c")).toBe("a < b > c"); + expect(stripHtml(""quoted"")).toBe('"quoted"'); + expect(stripHtml("it's")).toBe("it's"); + }); + + it("decodes numeric HTML entities", () => { + expect(stripHtml("© 2026")).toBe("\u00A9 2026"); + expect(stripHtml("❤")).toBe("\u2764"); + }); + + it("decodes entities inside HTML", () => { + expect(stripHtml("

Rock & Roll

")).toBe("Rock & Roll"); + }); + + it("preserves unknown entities as-is", () => { + expect(stripHtml("&unknown;")).toBe("&unknown;"); + }); + + it("strips nested tags", () => { + expect(stripHtml("

deep

")).toBe("deep"); + }); + + it("handles br with space before slash", () => { + expect(stripHtml("A
B")).toBe("A\nB"); + }); +}); + +// ============================================================================= +// AniListRecommendationClient Fetch Behavior Tests +// ============================================================================= + +describe("AniListRecommendationClient fetch behavior", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("passes AbortSignal.timeout to fetch", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ data: { Viewer: { id: 1, name: "test" } } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListRecommendationClient("test-token"); + await client.getViewerId(); + + expect(fetchSpy).toHaveBeenCalledOnce(); + const init = fetchSpy.mock.calls[0][1] as RequestInit; + expect(init.signal).toBeDefined(); + }); + + it("wraps timeout errors with descriptive message", async () => { + const timeoutError = new DOMException( + "The operation was aborted due to timeout", + "TimeoutError", + ); + vi.spyOn(globalThis, "fetch").mockRejectedValue(timeoutError); + + const client = new AniListRecommendationClient("test-token"); + await expect(client.getViewerId()).rejects.toThrow( + "AniList API request timed out after 30 seconds", + ); + }); + + it("re-throws non-timeout fetch errors as-is", async () => { + vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network failure")); + + const client = new AniListRecommendationClient("test-token"); + await expect(client.getViewerId()).rejects.toThrow("Network failure"); + }); + + it("retries once on 429 then succeeds", async () => { + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce(new Response("", { status: 429, headers: { "Retry-After": "0" } })) + .mockResolvedValueOnce( + new Response(JSON.stringify({ data: { Viewer: { id: 42, name: "test" } } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListRecommendationClient("test-token"); + const id = await client.getViewerId(); + + expect(id).toBe(42); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("throws RateLimitError after retry exhausted on 429", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("", { status: 429, headers: { "Retry-After": "0" } }), + ); + + const client = new AniListRecommendationClient("test-token"); + await expect(client.getViewerId()).rejects.toThrow("AniList rate limit exceeded"); + }); +}); + +// ============================================================================= +// Recommendation Pagination Tests +// ============================================================================= + +describe("AniListRecommendationClient pagination", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + function makeRecommendationResponse(nodes: Array<{ id: number }>, hasNextPage: boolean) { + return { + data: { + Media: { + id: 1, + title: { romaji: "Test", english: "Test" }, + recommendations: { + pageInfo: { hasNextPage }, + nodes: nodes.map((n) => ({ + rating: 10, + mediaRecommendation: { + id: n.id, + title: { romaji: `Rec ${n.id}` }, + coverImage: { large: null }, + description: null, + genres: [], + averageScore: 80, + siteUrl: `https://anilist.co/manga/${n.id}`, + }, + })), + }, + }, + }, + }; + } + + it("fetches multiple pages when hasNextPage is true", async () => { + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response(JSON.stringify(makeRecommendationResponse([{ id: 1 }, { id: 2 }], true)), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(makeRecommendationResponse([{ id: 3 }], false)), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListRecommendationClient("test-token"); + const nodes = await client.getRecommendationsForMedia(1, 10, 3); + + expect(nodes).toHaveLength(3); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("stops at maxPages even if hasNextPage is true", async () => { + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response(JSON.stringify(makeRecommendationResponse([{ id: 1 }], true)), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(makeRecommendationResponse([{ id: 2 }], true)), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListRecommendationClient("test-token"); + const nodes = await client.getRecommendationsForMedia(1, 10, 2); + + expect(nodes).toHaveLength(2); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("fetches single page when hasNextPage is false", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify(makeRecommendationResponse([{ id: 1 }], false)), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListRecommendationClient("test-token"); + const nodes = await client.getRecommendationsForMedia(1, 10, 3); + + expect(nodes).toHaveLength(1); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/recommendations-anilist/src/anilist.ts b/plugins/recommendations-anilist/src/anilist.ts new file mode 100644 index 00000000..2c3f7d24 --- /dev/null +++ b/plugins/recommendations-anilist/src/anilist.ts @@ -0,0 +1,301 @@ +/** + * AniList GraphQL API client for recommendations + * + * Uses AniList's recommendations and user list data to generate + * personalized manga suggestions. + */ + +import { ApiError, AuthError, RateLimitError } from "@ashdev/codex-plugin-sdk"; + +const ANILIST_API_URL = "https://graphql.anilist.co"; + +// ============================================================================= +// GraphQL Queries +// ============================================================================= + +const VIEWER_QUERY = ` + query { + Viewer { + id + name + } + } +`; + +/** Get recommendations for a specific manga */ +const MEDIA_RECOMMENDATIONS_QUERY = ` + query ($mediaId: Int!, $page: Int, $perPage: Int) { + Media(id: $mediaId, type: MANGA) { + id + title { + romaji + english + } + recommendations(page: $page, perPage: $perPage, sort: RATING_DESC) { + pageInfo { + hasNextPage + } + nodes { + rating + mediaRecommendation { + id + title { + romaji + english + } + coverImage { + large + } + description(asHtml: false) + genres + averageScore + siteUrl + } + } + } + } + } +`; + +/** Search for a manga by title to find its AniList ID */ +const SEARCH_MANGA_QUERY = ` + query ($search: String!) { + Media(search: $search, type: MANGA) { + id + title { + romaji + english + } + } + } +`; + +/** Get the user's manga list to know what they've already seen */ +const USER_MANGA_IDS_QUERY = ` + query ($userId: Int!, $page: Int, $perPage: Int) { + Page(page: $page, perPage: $perPage) { + pageInfo { + hasNextPage + currentPage + } + mediaList(userId: $userId, type: MANGA) { + mediaId + } + } + } +`; + +// ============================================================================= +// Types +// ============================================================================= + +export interface AniListRecommendationNode { + rating: number; + mediaRecommendation: { + id: number; + title: { romaji?: string; english?: string }; + coverImage: { large?: string }; + description: string | null; + genres: string[]; + averageScore: number | null; + siteUrl: string; + } | null; +} + +interface SearchResult { + id: number; + title: { romaji?: string; english?: string }; +} + +// ============================================================================= +// Client +// ============================================================================= + +export class AniListRecommendationClient { + private accessToken: string; + + constructor(accessToken: string) { + this.accessToken = accessToken; + } + + private async query(queryStr: string, variables?: Record): Promise { + return this.executeQuery(queryStr, variables, true); + } + + private async executeQuery( + queryStr: string, + variables: Record | undefined, + allowRetry: boolean, + ): Promise { + let response: Response; + try { + response = await fetch(ANILIST_API_URL, { + method: "POST", + signal: AbortSignal.timeout(30_000), + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${this.accessToken}`, + }, + body: JSON.stringify({ query: queryStr, variables }), + }); + } catch (error) { + if (error instanceof DOMException && error.name === "TimeoutError") { + throw new ApiError("AniList API request timed out after 30 seconds"); + } + throw error; + } + + if (response.status === 401) { + throw new AuthError("AniList access token is invalid or expired"); + } + + if (response.status === 429) { + const retryAfter = response.headers.get("Retry-After"); + const retrySeconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60; + const waitSeconds = Number.isNaN(retrySeconds) ? 60 : retrySeconds; + + if (allowRetry) { + await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); + return this.executeQuery(queryStr, variables, false); + } + + throw new RateLimitError(waitSeconds, "AniList rate limit exceeded"); + } + + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new ApiError( + `AniList API error: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`, + ); + } + + const json = (await response.json()) as { + data?: T; + errors?: Array<{ message: string }>; + }; + + if (json.errors?.length) { + const message = json.errors.map((e) => e.message).join("; "); + throw new ApiError(`AniList GraphQL error: ${message}`); + } + + if (!json.data) { + throw new ApiError("AniList returned empty data"); + } + + return json.data; + } + + /** Get the authenticated viewer's ID */ + async getViewerId(): Promise { + const data = await this.query<{ Viewer: { id: number; name: string } }>(VIEWER_QUERY); + return data.Viewer.id; + } + + /** Search for a manga by title and return its AniList ID */ + async searchManga(title: string): Promise { + try { + const data = await this.query<{ Media: SearchResult | null }>(SEARCH_MANGA_QUERY, { + search: title, + }); + return data.Media; + } catch { + return null; + } + } + + /** Get community recommendations for a specific manga (up to maxPages pages) */ + async getRecommendationsForMedia( + mediaId: number, + perPage = 10, + maxPages = 3, + ): Promise { + const allNodes: AniListRecommendationNode[] = []; + let page = 1; + let hasMore = true; + + while (hasMore && page <= maxPages) { + const data = await this.query<{ + Media: { + id: number; + title: { romaji?: string; english?: string }; + recommendations: { + pageInfo: { hasNextPage: boolean }; + nodes: AniListRecommendationNode[]; + }; + }; + }>(MEDIA_RECOMMENDATIONS_QUERY, { mediaId, page, perPage }); + + allNodes.push(...data.Media.recommendations.nodes); + hasMore = data.Media.recommendations.pageInfo.hasNextPage; + page++; + } + + return allNodes; + } + + /** Get all manga IDs in the user's list (for deduplication) */ + async getUserMangaIds(userId: number): Promise> { + const ids = new Set(); + let page = 1; + let hasMore = true; + + while (hasMore) { + const data = await this.query<{ + Page: { + pageInfo: { hasNextPage: boolean; currentPage: number }; + mediaList: Array<{ mediaId: number }>; + }; + }>(USER_MANGA_IDS_QUERY, { userId, page, perPage: 50 }); + + for (const entry of data.Page.mediaList) { + ids.add(entry.mediaId); + } + + hasMore = data.Page.pageInfo.hasNextPage; + page++; + } + + return ids; + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +/** Get the best title from an AniList title object */ +export function getBestTitle(title: { romaji?: string; english?: string }): string { + return title.english || title.romaji || "Unknown"; +} + +/** Common HTML entities to decode */ +const HTML_ENTITIES: Record = { + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'", + "'": "'", + " ": " ", + "—": "\u2014", + "–": "\u2013", + "…": "\u2026", +}; + +const ENTITY_PATTERN = /&(?:#(\d+)|#x([0-9a-fA-F]+)|[a-zA-Z]+);/g; + +/** Strip HTML tags and decode HTML entities */ +export function stripHtml(html: string | null): string | undefined { + if (!html) return undefined; + return html + .replace(//gi, "\n") + .replace(/<[^>]*>/g, "") + .replace(ENTITY_PATTERN, (match, decimal, hex) => { + if (decimal) return String.fromCharCode(Number.parseInt(decimal, 10)); + if (hex) return String.fromCharCode(Number.parseInt(hex, 16)); + return HTML_ENTITIES[match] ?? match; + }) + .trim(); +} diff --git a/plugins/recommendations-anilist/src/index.test.ts b/plugins/recommendations-anilist/src/index.test.ts new file mode 100644 index 00000000..8fb729e3 --- /dev/null +++ b/plugins/recommendations-anilist/src/index.test.ts @@ -0,0 +1,699 @@ +import { EXTERNAL_ID_SOURCE_ANILIST, type UserLibraryEntry } from "@ashdev/codex-plugin-sdk"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { AniListRecommendationNode } from "./anilist.js"; +import { + convertRecommendations, + dismissedIds, + pickSeedEntries, + resolveAniListIds, + setClient, + setSearchFallback, +} from "./index.js"; + +// ============================================================================= +// Helpers +// ============================================================================= + +function makeEntry(overrides: Partial & { seriesId: string }): UserLibraryEntry { + return { + title: `Series ${overrides.seriesId}`, + alternateTitles: [], + genres: [], + tags: [], + externalIds: [], + booksRead: 0, + booksOwned: 0, + ...overrides, + }; +} + +function makeNode( + overrides: Partial<{ + id: number; + rating: number; + averageScore: number | null; + title: string; + genres: string[]; + description: string | null; + siteUrl: string; + coverImage: string | null; + mediaRecommendation: AniListRecommendationNode["mediaRecommendation"]; + }>, +): AniListRecommendationNode { + // Allow passing null mediaRecommendation explicitly + if ("mediaRecommendation" in overrides && overrides.mediaRecommendation === null) { + return { rating: overrides.rating ?? 50, mediaRecommendation: null }; + } + return { + rating: overrides.rating ?? 50, + mediaRecommendation: { + id: overrides.id ?? 100, + title: { english: overrides.title ?? "Recommended Manga" }, + coverImage: { + large: + "coverImage" in overrides + ? (overrides.coverImage ?? undefined) + : "https://img.example.com/cover.jpg", + }, + description: "description" in overrides ? (overrides.description ?? null) : "A great manga", + genres: overrides.genres ?? ["Action"], + averageScore: "averageScore" in overrides ? (overrides.averageScore ?? null) : 80, + siteUrl: overrides.siteUrl ?? `https://anilist.co/manga/${overrides.id ?? 100}`, + }, + }; +} + +// ============================================================================= +// pickSeedEntries Tests +// ============================================================================= + +describe("pickSeedEntries", () => { + it("returns empty array for empty library", () => { + expect(pickSeedEntries([], 10)).toEqual([]); + }); + + it("returns all entries when fewer than maxSeeds", () => { + const entries = [makeEntry({ seriesId: "a", userRating: 80, booksRead: 5 })]; + const result = pickSeedEntries(entries, 10); + expect(result).toHaveLength(1); + expect(result[0].seriesId).toBe("a"); + }); + + it("limits to maxSeeds", () => { + const entries = Array.from({ length: 20 }, (_, i) => + makeEntry({ seriesId: `s${i}`, userRating: 50, booksRead: 1 }), + ); + const result = pickSeedEntries(entries, 5); + expect(result).toHaveLength(5); + }); + + it("prioritizes by rating descending", () => { + const entries = [ + makeEntry({ seriesId: "low", userRating: 30, booksRead: 10 }), + makeEntry({ seriesId: "high", userRating: 90, booksRead: 1 }), + makeEntry({ seriesId: "mid", userRating: 60, booksRead: 5 }), + ]; + const result = pickSeedEntries(entries, 3); + expect(result.map((e) => e.seriesId)).toEqual(["high", "mid", "low"]); + }); + + it("breaks ties by booksRead descending", () => { + const entries = [ + makeEntry({ seriesId: "fewer", userRating: 80, booksRead: 2 }), + makeEntry({ seriesId: "more", userRating: 80, booksRead: 10 }), + ]; + const result = pickSeedEntries(entries, 2); + expect(result[0].seriesId).toBe("more"); + expect(result[1].seriesId).toBe("fewer"); + }); + + it("treats undefined rating as 0", () => { + const entries = [ + makeEntry({ seriesId: "rated", userRating: 50, booksRead: 1 }), + makeEntry({ seriesId: "unrated", booksRead: 1 }), + ]; + const result = pickSeedEntries(entries, 2); + expect(result[0].seriesId).toBe("rated"); + expect(result[1].seriesId).toBe("unrated"); + }); + + it("does not mutate the original array", () => { + const entries = [ + makeEntry({ seriesId: "b", userRating: 90, booksRead: 1 }), + makeEntry({ seriesId: "a", userRating: 50, booksRead: 1 }), + ]; + const originalOrder = entries.map((e) => e.seriesId); + pickSeedEntries(entries, 2); + expect(entries.map((e) => e.seriesId)).toEqual(originalOrder); + }); +}); + +// ============================================================================= +// convertRecommendations Tests +// ============================================================================= + +describe("convertRecommendations", () => { + beforeEach(() => { + dismissedIds.clear(); + }); + + it("computes score from community rating and average score", () => { + // rating=80, averageScore=70 → communityScore=0.8, avgScore=0.7 + // score = round((0.8*0.6 + 0.7*0.4) * 100) / 100 = round((0.48+0.28)*100)/100 = 0.76 + const nodes = [makeNode({ id: 1, rating: 80, averageScore: 70 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].score).toBe(0.76); + }); + + it("uses 0.5 fallback when averageScore is null", () => { + // rating=100, averageScore=null → communityScore=1.0, avgScore=0.5 + // score = round((1.0*0.6 + 0.5*0.4)*100)/100 = round(0.8*100)/100 = 0.8 + const nodes = [makeNode({ id: 1, rating: 100, averageScore: null })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].score).toBe(0.8); + }); + + it("uses 0.5 fallback when averageScore is 0 (falsy)", () => { + // rating=0, averageScore=0 → communityScore=0, avgScore=0.5 (0 is falsy → fallback) + // score = round((0*0.6 + 0.5*0.4)*100)/100 = 0.2 + const nodes = [makeNode({ id: 1, rating: 0, averageScore: 0 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].score).toBe(0.2); + }); + + it("clamps negative community rating to 0", () => { + // rating=-50 → communityScore = max(0, min(-50,100))/100 = 0 + // averageScore=80 → avgScore = 0.8 + // score = round((0*0.6 + 0.8*0.4)*100)/100 = 0.32 + const nodes = [makeNode({ id: 1, rating: -50, averageScore: 80 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].score).toBe(0.32); + }); + + it("clamps community rating above 100 to 100", () => { + // rating=200 → communityScore = max(0, min(200,100))/100 = 1.0 + // averageScore=100 → avgScore = 1.0 + // score = round((1.0*0.6 + 1.0*0.4)*100)/100 = 1.0 + const nodes = [makeNode({ id: 1, rating: 200, averageScore: 100 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].score).toBe(1.0); + }); + + it("clamps final score to [0, 1]", () => { + // Even with maximum inputs, score should not exceed 1.0 + const nodes = [makeNode({ id: 1, rating: 100, averageScore: 100 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results[0].score).toBeLessThanOrEqual(1.0); + expect(results[0].score).toBeGreaterThanOrEqual(0); + }); + + it("excludes nodes with IDs in excludeIds", () => { + const nodes = [makeNode({ id: 1, rating: 80 }), makeNode({ id: 2, rating: 90 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set(["1"])); + expect(results).toHaveLength(1); + expect(results[0].externalId).toBe("2"); + }); + + it("excludes nodes with IDs in dismissedIds", () => { + dismissedIds.add("1"); + const nodes = [makeNode({ id: 1, rating: 80 }), makeNode({ id: 2, rating: 90 })]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].externalId).toBe("2"); + }); + + it("filters out nodes with null mediaRecommendation", () => { + const nodes = [ + makeNode({ mediaRecommendation: null, rating: 50 }), + makeNode({ id: 2, rating: 90 }), + ]; + const results = convertRecommendations(nodes, "Berserk", new Set(), new Set()); + expect(results).toHaveLength(1); + expect(results[0].externalId).toBe("2"); + }); + + it("sets inLibrary flag when manga is in userMangaIds", () => { + const nodes = [makeNode({ id: 1, rating: 80 }), makeNode({ id: 2, rating: 80 })]; + const userMangaIds = new Set([1]); + const results = convertRecommendations(nodes, "Berserk", userMangaIds, new Set()); + expect(results[0].inLibrary).toBe(true); + expect(results[1].inLibrary).toBe(false); + }); + + it("returns empty array for empty nodes", () => { + const results = convertRecommendations([], "Berserk", new Set(), new Set()); + expect(results).toEqual([]); + }); + + it("constructs basedOn field from basedOnTitle", () => { + const nodes = [makeNode({ id: 1, rating: 80 })]; + const results = convertRecommendations(nodes, "Attack on Titan", new Set(), new Set()); + expect(results[0].basedOn).toEqual(["Attack on Titan"]); + expect(results[0].reason).toBe("Recommended because you liked Attack on Titan"); + }); + + it("sets externalId, externalUrl, title, coverUrl, summary, genres", () => { + const nodes = [ + makeNode({ + id: 42, + rating: 50, + averageScore: 60, + title: "One Piece", + genres: ["Adventure", "Comedy"], + description: "A pirate adventure", + siteUrl: "https://anilist.co/manga/42", + coverImage: "https://img.example.com/onepiece.jpg", + }), + ]; + const results = convertRecommendations(nodes, "Naruto", new Set(), new Set()); + expect(results).toHaveLength(1); + const rec = results[0]; + expect(rec.externalId).toBe("42"); + expect(rec.externalUrl).toBe("https://anilist.co/manga/42"); + expect(rec.title).toBe("One Piece"); + expect(rec.coverUrl).toBe("https://img.example.com/onepiece.jpg"); + expect(rec.summary).toBe("A pirate adventure"); + expect(rec.genres).toEqual(["Adventure", "Comedy"]); + }); + + it("handles coverImage.large being undefined", () => { + const nodes = [makeNode({ id: 1, rating: 50, coverImage: null })]; + // coverImage.large is null → undefined via ?? undefined + const results = convertRecommendations(nodes, "Test", new Set(), new Set()); + expect(results[0].coverUrl).toBeUndefined(); + }); + + it("handles null description via stripHtml", () => { + const nodes = [makeNode({ id: 1, rating: 50, description: null })]; + const results = convertRecommendations(nodes, "Test", new Set(), new Set()); + expect(results[0].summary).toBeUndefined(); + }); +}); + +// ============================================================================= +// resolveAniListIds Tests +// ============================================================================= + +describe("resolveAniListIds", () => { + afterEach(() => { + setClient(null); + }); + + function makeMockClient(overrides: { + searchManga?: ( + title: string, + ) => Promise<{ id: number; title: { romaji?: string; english?: string } } | null>; + }) { + return { + getViewerId: vi.fn(), + getRecommendationsForMedia: vi.fn(), + getUserMangaIds: vi.fn(), + searchManga: overrides.searchManga ?? vi.fn().mockResolvedValue(null), + } as unknown as Parameters[0]; + } + + it("resolves entries with api:anilist external ID directly", async () => { + const mockClient = makeMockClient({}); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Berserk", + userRating: 90, + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "21" }], + }), + ]; + + const result = await resolveAniListIds(entries); + expect(result.size).toBe(1); + expect(result.get("s1")).toEqual({ anilistId: 21, title: "Berserk", rating: 90 }); + // Should NOT call searchManga since external ID was found + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); + + it("resolves entries with legacy 'anilist' source", async () => { + const mockClient = makeMockClient({}); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Berserk", + externalIds: [{ source: "anilist", externalId: "21" }], + }), + ]; + + const result = await resolveAniListIds(entries); + expect(result.size).toBe(1); + expect(result.get("s1")?.anilistId).toBe(21); + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); + + it("resolves entries with legacy 'AniList' source (case variation)", async () => { + const mockClient = makeMockClient({}); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Berserk", + externalIds: [{ source: "AniList", externalId: "21" }], + }), + ]; + + const result = await resolveAniListIds(entries); + expect(result.size).toBe(1); + expect(result.get("s1")?.anilistId).toBe(21); + }); + + it("falls back to title search when no external IDs", async () => { + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 42, title: { english: "Berserk" } }), + }); + setClient(mockClient); + + const entries = [makeEntry({ seriesId: "s1", title: "Berserk", userRating: 75 })]; + + const result = await resolveAniListIds(entries); + expect(result.size).toBe(1); + expect(result.get("s1")).toEqual({ anilistId: 42, title: "Berserk", rating: 75 }); + expect(mockClient.searchManga).toHaveBeenCalledWith("Berserk"); + }); + + it("skips entry when external ID is non-numeric and falls back to search", async () => { + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 99, title: { english: "Test" } }), + }); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Test", + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "not-a-number" }], + }), + ]; + + const result = await resolveAniListIds(entries); + // Should fall through to search since NaN is not valid + expect(result.size).toBe(1); + expect(result.get("s1")?.anilistId).toBe(99); + expect(mockClient.searchManga).toHaveBeenCalledWith("Test"); + }); + + it("skips entry when search returns null", async () => { + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue(null), + }); + setClient(mockClient); + + const entries = [makeEntry({ seriesId: "s1", title: "Obscure Manga" })]; + + const result = await resolveAniListIds(entries); + expect(result.size).toBe(0); + }); + + it("preserves rating and title in result map", async () => { + const mockClient = makeMockClient({}); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "My Manga", + userRating: 85, + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "55" }], + }), + ]; + + const result = await resolveAniListIds(entries); + const entry = result.get("s1"); + expect(entry?.title).toBe("My Manga"); + expect(entry?.rating).toBe(85); + }); + + it("treats undefined userRating as 0", async () => { + const mockClient = makeMockClient({}); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Unrated", + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "10" }], + }), + ]; + + const result = await resolveAniListIds(entries); + expect(result.get("s1")?.rating).toBe(0); + }); + + it("throws when client is not initialized", async () => { + setClient(null); + await expect(resolveAniListIds([])).rejects.toThrow("Plugin not initialized"); + }); + + it("resolves multiple entries", async () => { + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 77, title: { english: "Found" } }), + }); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Has ID", + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "10" }], + }), + makeEntry({ seriesId: "s2", title: "Needs Search" }), + makeEntry({ seriesId: "s3", title: "Also Needs Search" }), + ]; + + const result = await resolveAniListIds(entries); + expect(result.size).toBe(3); + expect(result.get("s1")?.anilistId).toBe(10); + expect(result.get("s2")?.anilistId).toBe(77); + expect(result.get("s3")?.anilistId).toBe(77); + expect(mockClient.searchManga).toHaveBeenCalledTimes(2); + }); +}); + +// ============================================================================= +// resolveAniListIds — searchFallback toggle Tests +// ============================================================================= + +describe("resolveAniListIds searchFallback toggle", () => { + afterEach(() => { + setClient(null); + setSearchFallback(true); // restore default + }); + + function makeMockClient(overrides: { + searchManga?: ( + title: string, + ) => Promise<{ id: number; title: { romaji?: string; english?: string } } | null>; + }) { + return { + getViewerId: vi.fn(), + getRecommendationsForMedia: vi.fn(), + getUserMangaIds: vi.fn(), + searchManga: overrides.searchManga ?? vi.fn().mockResolvedValue(null), + } as unknown as Parameters[0]; + } + + it("calls searchManga when searchFallback is true and no external ID", async () => { + setSearchFallback(true); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 42, title: { english: "Berserk" } }), + }); + setClient(mockClient); + + const entries = [makeEntry({ seriesId: "s1", title: "Berserk", userRating: 75 })]; + const result = await resolveAniListIds(entries); + + expect(result.size).toBe(1); + expect(result.get("s1")?.anilistId).toBe(42); + expect(mockClient.searchManga).toHaveBeenCalledWith("Berserk"); + }); + + it("skips searchManga when searchFallback is false and no external ID", async () => { + setSearchFallback(false); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 42, title: { english: "Berserk" } }), + }); + setClient(mockClient); + + const entries = [makeEntry({ seriesId: "s1", title: "Berserk", userRating: 75 })]; + const result = await resolveAniListIds(entries); + + expect(result.size).toBe(0); + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); + + it("still resolves via external ID when searchFallback is false", async () => { + setSearchFallback(false); + const mockClient = makeMockClient({}); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Berserk", + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "21" }], + }), + ]; + const result = await resolveAniListIds(entries); + + expect(result.size).toBe(1); + expect(result.get("s1")?.anilistId).toBe(21); + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); + + it("mixes matched and unmatched entries with searchFallback disabled", async () => { + setSearchFallback(false); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 99, title: { english: "Found" } }), + }); + setClient(mockClient); + + const entries = [ + makeEntry({ + seriesId: "s1", + title: "Has ID", + externalIds: [{ source: EXTERNAL_ID_SOURCE_ANILIST, externalId: "10" }], + }), + makeEntry({ seriesId: "s2", title: "No ID" }), + ]; + const result = await resolveAniListIds(entries); + + expect(result.size).toBe(1); + expect(result.has("s1")).toBe(true); + expect(result.has("s2")).toBe(false); + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); +}); + +// ============================================================================= +// Score Merging Tests (via convertRecommendations + simulated merge loop) +// ============================================================================= + +describe("score merging", () => { + beforeEach(() => { + dismissedIds.clear(); + }); + + /** + * Simulate the merge loop from provider.get(): + * For each rec, if externalId already exists, boost score by 0.05 and merge basedOn. + */ + function mergeRecommendations( + recsBySource: Array<{ basedOnTitle: string; nodes: AniListRecommendationNode[] }>, + userMangaIds = new Set(), + excludeIds = new Set(), + ): Map { + const allRecs = new Map(); + + for (const { basedOnTitle, nodes } of recsBySource) { + const recs = convertRecommendations(nodes, basedOnTitle, userMangaIds, excludeIds); + for (const rec of recs) { + const existing = allRecs.get(rec.externalId); + if (existing) { + const mergedBasedOn = [...new Set([...existing.basedOn, ...rec.basedOn])]; + const boostedScore = Math.min(existing.score + 0.05, 1.0); + allRecs.set(rec.externalId, { + score: Math.round(boostedScore * 100) / 100, + basedOn: mergedBasedOn, + reason: + mergedBasedOn.length > 1 + ? `Recommended based on ${mergedBasedOn.join(", ")}` + : existing.reason, + }); + } else { + allRecs.set(rec.externalId, { + score: rec.score, + basedOn: rec.basedOn, + reason: rec.reason, + }); + } + } + } + + return allRecs; + } + + it("boosts score by 0.05 for duplicate recommendation", () => { + // Same manga recommended from two different seeds + // rating=80, averageScore=80 → communityScore=0.8, avgScore=0.8 + // score = round((0.8*0.6 + 0.8*0.4)*100)/100 = 0.8 + const node = makeNode({ id: 1, rating: 80, averageScore: 80 }); + const merged = mergeRecommendations([ + { basedOnTitle: "Berserk", nodes: [node] }, + { basedOnTitle: "Vagabond", nodes: [node] }, + ]); + + const rec = merged.get("1"); + expect(rec).toBeDefined(); + // 0.8 + 0.05 = 0.85 + expect(rec?.score).toBe(0.85); + }); + + it("clamps boosted score at 1.0", () => { + // rating=100, averageScore=100 → score = 1.0 + const node = makeNode({ id: 1, rating: 100, averageScore: 100 }); + const merged = mergeRecommendations([ + { basedOnTitle: "Seed A", nodes: [node] }, + { basedOnTitle: "Seed B", nodes: [node] }, + ]); + + const rec = merged.get("1"); + expect(rec).toBeDefined(); + // 1.0 + 0.05 → clamped to 1.0 + expect(rec?.score).toBe(1.0); + }); + + it("merges and deduplicates basedOn arrays", () => { + const node = makeNode({ id: 1, rating: 50 }); + const merged = mergeRecommendations([ + { basedOnTitle: "Berserk", nodes: [node] }, + { basedOnTitle: "Vagabond", nodes: [node] }, + ]); + + const rec = merged.get("1"); + expect(rec).toBeDefined(); + expect(rec?.basedOn).toEqual(["Berserk", "Vagabond"]); + }); + + it("updates reason text with multiple sources", () => { + const node = makeNode({ id: 1, rating: 50 }); + const merged = mergeRecommendations([ + { basedOnTitle: "Berserk", nodes: [node] }, + { basedOnTitle: "Vagabond", nodes: [node] }, + ]); + + const rec = merged.get("1"); + expect(rec).toBeDefined(); + expect(rec?.reason).toBe("Recommended based on Berserk, Vagabond"); + }); + + it("applies two boosts for triple-recommended manga", () => { + // rating=60, averageScore=70 → communityScore=0.6, avgScore=0.7 + // score = round((0.6*0.6 + 0.7*0.4)*100)/100 = round((0.36+0.28)*100)/100 = 0.64 + const node = makeNode({ id: 1, rating: 60, averageScore: 70 }); + const merged = mergeRecommendations([ + { basedOnTitle: "Seed A", nodes: [node] }, + { basedOnTitle: "Seed B", nodes: [node] }, + { basedOnTitle: "Seed C", nodes: [node] }, + ]); + + const rec = merged.get("1"); + expect(rec).toBeDefined(); + // 0.64 + 0.05 + 0.05 = 0.74 + expect(rec?.score).toBe(0.74); + expect(rec?.basedOn).toEqual(["Seed A", "Seed B", "Seed C"]); + expect(rec?.reason).toBe("Recommended based on Seed A, Seed B, Seed C"); + }); + + it("does not boost same basedOn title appearing twice", () => { + const node = makeNode({ id: 1, rating: 50, averageScore: 50 }); + const merged = mergeRecommendations([ + { basedOnTitle: "Berserk", nodes: [node] }, + { basedOnTitle: "Berserk", nodes: [node] }, + ]); + + const rec = merged.get("1"); + expect(rec).toBeDefined(); + // basedOn is deduplicated + expect(rec?.basedOn).toEqual(["Berserk"]); + // Score is still boosted (different source instances) + // 0.5 + 0.05 = 0.55 + expect(rec?.score).toBe(0.55); + // Single basedOn means reason stays as original + expect(rec?.reason).toBe("Recommended because you liked Berserk"); + }); +}); diff --git a/plugins/recommendations-anilist/src/index.ts b/plugins/recommendations-anilist/src/index.ts new file mode 100644 index 00000000..8a999eca --- /dev/null +++ b/plugins/recommendations-anilist/src/index.ts @@ -0,0 +1,355 @@ +/** + * AniList Recommendations Plugin for Codex + * + * Generates personalized manga recommendations by: + * 1. Matching user's library entries to AniList manga IDs + * 2. Fetching community recommendations for highly-rated titles + * 3. Scoring and deduplicating results + * 4. Returning the top recommendations + * + * Communicates via JSON-RPC over stdio using the Codex plugin SDK. + */ + +import { + createLogger, + createRecommendationPlugin, + EXTERNAL_ID_SOURCE_ANILIST, + type InitializeParams, + type PluginStorage, + type Recommendation, + type RecommendationClearResponse, + type RecommendationDismissRequest, + type RecommendationDismissResponse, + type RecommendationProvider, + type RecommendationRequest, + type RecommendationResponse, + type UserLibraryEntry, +} from "@ashdev/codex-plugin-sdk"; +import { + AniListRecommendationClient, + type AniListRecommendationNode, + getBestTitle, + stripHtml, +} from "./anilist.js"; +import { manifest } from "./manifest.js"; + +const logger = createLogger({ name: "recommendations-anilist", level: "debug" }); + +// Plugin state (set during initialization) +let client: AniListRecommendationClient | null = null; +let viewerId: number | null = null; +let maxRecommendations = 20; +let maxSeeds = 10; +let searchFallback = true; +let storage: PluginStorage | null = null; + +/** Set the AniList client (exported for testing) */ +export function setClient(c: AniListRecommendationClient | null): void { + client = c; +} + +/** Set the searchFallback flag (exported for testing) */ +export function setSearchFallback(enabled: boolean): void { + searchFallback = enabled; +} + +/** Storage key for persisted dismissed recommendation IDs */ +const DISMISSED_STORAGE_KEY = "dismissed_ids"; + +// In-memory cache of dismissed IDs (synced with storage). +// Loaded from storage on initialize, updated on dismiss/clear. +export const dismissedIds = new Set(); + +/** + * Load dismissed IDs from persistent storage into the in-memory cache. + */ +async function loadDismissedIds(): Promise { + if (!storage) return; + try { + const result = await storage.get(DISMISSED_STORAGE_KEY); + if (Array.isArray(result.data)) { + dismissedIds.clear(); + for (const id of result.data) { + if (typeof id === "string") { + dismissedIds.add(id); + } + } + logger.debug(`Loaded ${dismissedIds.size} dismissed IDs from storage`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : "Unknown error"; + logger.warn(`Failed to load dismissed IDs from storage: ${msg}`); + } +} + +/** + * Persist the current dismissed IDs set to storage. + */ +async function saveDismissedIds(): Promise { + if (!storage) return; + try { + await storage.set(DISMISSED_STORAGE_KEY, [...dismissedIds]); + } catch (err) { + const msg = err instanceof Error ? err.message : "Unknown error"; + logger.warn(`Failed to save dismissed IDs to storage: ${msg}`); + } +} + +// ============================================================================= +// Recommendation Generation +// ============================================================================= + +/** + * Find AniList IDs for library entries. + * Tries external_ids first, falls back to title search. + */ +export async function resolveAniListIds( + entries: UserLibraryEntry[], +): Promise> { + if (!client) throw new Error("Plugin not initialized"); + + const resolved = new Map(); + + for (const entry of entries) { + // Check if we already have an AniList external ID + // Prefer api:anilist (new convention), fall back to legacy source names + const anilistExt = entry.externalIds?.find( + (e) => + e.source === EXTERNAL_ID_SOURCE_ANILIST || e.source === "anilist" || e.source === "AniList", + ); + + if (anilistExt) { + const id = Number.parseInt(anilistExt.externalId, 10); + if (!Number.isNaN(id)) { + resolved.set(entry.seriesId, { + anilistId: id, + title: entry.title, + rating: entry.userRating ?? 0, + }); + continue; + } + } + + // Fall back to title search (when enabled) + if (searchFallback) { + const result = await client.searchManga(entry.title); + if (result) { + resolved.set(entry.seriesId, { + anilistId: result.id, + title: entry.title, + rating: entry.userRating ?? 0, + }); + } + } + } + + return resolved; +} + +/** + * Pick the best entries from the user's library to seed recommendations. + * Prioritizes highly-rated, recently-read titles. + */ +export function pickSeedEntries(entries: UserLibraryEntry[], maxSeeds: number): UserLibraryEntry[] { + // Sort by rating (desc), then by recency + const sorted = [...entries].sort((a, b) => { + const ratingDiff = (b.userRating ?? 0) - (a.userRating ?? 0); + if (ratingDiff !== 0) return ratingDiff; + // Fall back to books read as a proxy for engagement + return b.booksRead - a.booksRead; + }); + + return sorted.slice(0, maxSeeds); +} + +/** + * Convert AniList recommendation nodes into Recommendation objects. + */ +export function convertRecommendations( + nodes: AniListRecommendationNode[], + basedOnTitle: string, + userMangaIds: Set, + excludeIds: Set, +): Recommendation[] { + const results: Recommendation[] = []; + + for (const node of nodes) { + if (!node.mediaRecommendation) continue; + + const media = node.mediaRecommendation; + const externalId = String(media.id); + + // Skip if excluded or dismissed + if (excludeIds.has(externalId) || dismissedIds.has(externalId)) continue; + + const inLibrary = userMangaIds.has(media.id); + + // Compute a relevance score based on community rating and AniList average score + const communityScore = Math.max(0, Math.min(node.rating, 100)) / 100; + const avgScore = media.averageScore ? media.averageScore / 100 : 0.5; + const score = Math.round((communityScore * 0.6 + avgScore * 0.4) * 100) / 100; + + results.push({ + externalId, + externalUrl: media.siteUrl, + title: getBestTitle(media.title), + coverUrl: media.coverImage.large ?? undefined, + summary: stripHtml(media.description), + genres: media.genres ?? [], + score: Math.max(0, Math.min(score, 1)), + reason: `Recommended because you liked ${basedOnTitle}`, + basedOn: [basedOnTitle], + inLibrary, + }); + } + + return results; +} + +// ============================================================================= +// Provider Implementation +// ============================================================================= + +const provider: RecommendationProvider = { + async get(params: RecommendationRequest): Promise { + if (!client) { + throw new Error("Plugin not initialized - no AniList client"); + } + + if (viewerId === null) { + viewerId = await client.getViewerId(); + logger.info(`Authenticated as viewer ${viewerId}`); + } + + const { library, limit, excludeIds: rawExcludeIds = [] } = params; + const effectiveLimit = Math.min(limit ?? maxRecommendations, 50); + const excludeIds = new Set(rawExcludeIds); + + // Return early if library is empty — no seeds to work with + if (!library || library.length === 0) { + logger.info("Empty library — returning no recommendations"); + return { recommendations: [], generatedAt: new Date().toISOString(), cached: false }; + } + + // Get user's existing manga IDs for dedup + const userMangaIds = await client.getUserMangaIds(viewerId); + logger.debug(`User has ${userMangaIds.size} manga in AniList list`); + + // Pick seed entries (top-rated from user's library) + const seeds = pickSeedEntries(library, maxSeeds); + logger.debug(`Using ${seeds.length} seed entries from library of ${library.length}`); + + // Resolve AniList IDs for seed entries + const resolved = await resolveAniListIds(seeds); + logger.debug(`Resolved ${resolved.size} AniList IDs from ${seeds.length} seeds`); + + // Fetch recommendations for each seed + const allRecs = new Map(); + + for (const [, { anilistId, title }] of resolved) { + try { + const nodes = await client.getRecommendationsForMedia(anilistId, 10); + const recs = convertRecommendations(nodes, title, userMangaIds, excludeIds); + + for (const rec of recs) { + // If we've seen this recommendation before, merge basedOn and keep higher score + const existing = allRecs.get(rec.externalId); + if (existing) { + // Merge basedOn titles + const mergedBasedOn = [...new Set([...existing.basedOn, ...rec.basedOn])]; + // Boost score slightly for multiply-recommended titles + const boostedScore = Math.min(existing.score + 0.05, 1.0); + allRecs.set(rec.externalId, { + ...existing, + score: Math.round(boostedScore * 100) / 100, + basedOn: mergedBasedOn, + reason: + mergedBasedOn.length > 1 + ? `Recommended based on ${mergedBasedOn.join(", ")}` + : existing.reason, + }); + } else { + allRecs.set(rec.externalId, rec); + } + } + } catch (error) { + const msg = error instanceof Error ? error.message : "Unknown error"; + logger.warn(`Failed to get recommendations for AniList ID ${anilistId}: ${msg}`); + } + } + + // Sort by score descending and take top N + const sorted = [...allRecs.values()].sort((a, b) => b.score - a.score).slice(0, effectiveLimit); + + logger.info(`Generated ${sorted.length} recommendations from ${resolved.size} seed titles`); + + return { + recommendations: sorted, + generatedAt: new Date().toISOString(), + cached: false, + }; + }, + + async dismiss(params: RecommendationDismissRequest): Promise { + dismissedIds.add(params.externalId); + logger.debug( + `Dismissed recommendation: ${params.externalId} (reason: ${params.reason ?? "none"})`, + ); + await saveDismissedIds(); + return { dismissed: true }; + }, + + async clear(): Promise { + const count = dismissedIds.size; + dismissedIds.clear(); + logger.info(`Cleared ${count} dismissed recommendations`); + await saveDismissedIds(); + return { cleared: true }; + }, +}; + +// ============================================================================= +// Plugin Initialization +// ============================================================================= + +createRecommendationPlugin({ + manifest, + provider, + logLevel: "debug", + async onInitialize(params: InitializeParams) { + const accessToken = params.credentials?.access_token; + if (accessToken) { + client = new AniListRecommendationClient(accessToken); + logger.info("AniList client initialized with access token"); + } else { + logger.warn("No access token provided - recommendation operations will fail"); + } + + // Read maxRecommendations from adminConfig (defined in configSchema) + const rawMax = params.adminConfig?.maxRecommendations; + if (typeof rawMax === "number") { + maxRecommendations = Math.max(1, Math.min(Math.round(rawMax), 50)); + logger.info(`Max recommendations set to: ${maxRecommendations}`); + } + + // Read maxSeeds from adminConfig (defined in configSchema) + const rawSeeds = params.adminConfig?.maxSeeds; + if (typeof rawSeeds === "number") { + maxSeeds = Math.max(1, Math.min(Math.round(rawSeeds), 25)); + logger.info(`Max seeds set to: ${maxSeeds}`); + } + + // Read searchFallback from userConfig (default: true — preserve existing behavior) + const uc = params.userConfig; + if (uc && typeof uc.searchFallback === "boolean") { + searchFallback = uc.searchFallback; + logger.info(`Search fallback set to: ${searchFallback}`); + } + + // Capture the storage client and restore persisted dismissed IDs + storage = params.storage; + await loadDismissedIds(); + }, +}); + +logger.info("AniList recommendations plugin started"); diff --git a/plugins/recommendations-anilist/src/manifest.ts b/plugins/recommendations-anilist/src/manifest.ts new file mode 100644 index 00000000..f79a9400 --- /dev/null +++ b/plugins/recommendations-anilist/src/manifest.ts @@ -0,0 +1,74 @@ +import type { PluginManifest } from "@ashdev/codex-plugin-sdk"; +import packageJson from "../package.json" with { type: "json" }; + +export const manifest = { + name: "recommendations-anilist", + displayName: "AniList Recommendations", + version: packageJson.version, + description: + "Personalized manga recommendations from AniList based on your reading history and ratings.", + author: "Codex", + homepage: "https://github.com/AshDevFr/codex", + protocolVersion: "1.0", + capabilities: { + userRecommendationProvider: true, + }, + requiredCredentials: [ + { + key: "access_token", + label: "AniList Access Token", + description: "OAuth access token for AniList API", + type: "password" as const, + required: true, + sensitive: true, + }, + ], + configSchema: { + description: "Recommendation configuration", + fields: [ + { + key: "maxRecommendations", + label: "Maximum Recommendations", + description: "Maximum number of recommendations to generate (1-50)", + type: "number" as const, + required: false, + default: 20, + }, + { + key: "maxSeeds", + label: "Maximum Seed Titles", + description: "Number of top-rated library titles used to generate recommendations (1-25)", + type: "number" as const, + required: false, + default: 10, + }, + ], + }, + userConfigSchema: { + description: "Per-user recommendation settings", + fields: [ + { + key: "searchFallback", + label: "Search Fallback", + description: + "When a series has no AniList ID, search by title to find a match. Disable for strict matching only.", + type: "boolean" as const, + required: false, + default: true, + }, + ], + }, + oauth: { + authorizationUrl: "https://anilist.co/api/v2/oauth/authorize", + tokenUrl: "https://anilist.co/api/v2/oauth/token", + scopes: [], + pkce: false, + }, + userDescription: "Personalized manga recommendations powered by AniList community data", + adminSetupInstructions: + "To enable OAuth login, create an AniList API client at https://anilist.co/settings/developer. Set the redirect URL to {your-codex-url}/api/v1/user/plugins/oauth/callback. Enter the Client ID below. Without OAuth configured, users can still connect by pasting a personal access token.", + userSetupInstructions: + "Connect your AniList account via OAuth, or paste a personal access token. To generate a token, visit https://anilist.co/settings/developer, create a client with redirect URL https://anilist.co/api/v2/oauth/pin, then authorize it to receive your token.", +} as const satisfies PluginManifest & { + capabilities: { userRecommendationProvider: true }; +}; diff --git a/plugins/recommendations-anilist/tsconfig.json b/plugins/recommendations-anilist/tsconfig.json new file mode 100644 index 00000000..ef1ca5f9 --- /dev/null +++ b/plugins/recommendations-anilist/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/plugins/recommendations-anilist/vitest.config.ts b/plugins/recommendations-anilist/vitest.config.ts new file mode 100644 index 00000000..ae847ff6 --- /dev/null +++ b/plugins/recommendations-anilist/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/plugins/sdk-typescript/README.md b/plugins/sdk-typescript/README.md index a1e8b499..941dfa22 100644 --- a/plugins/sdk-typescript/README.md +++ b/plugins/sdk-typescript/README.md @@ -75,7 +75,6 @@ const provider: MetadataProvider = { async match(params) { const result = await this.search({ query: params.title, - contentType: params.contentType, }); return { diff --git a/plugins/sdk-typescript/package-lock.json b/plugins/sdk-typescript/package-lock.json index 4ab4bfb4..6d626410 100644 --- a/plugins/sdk-typescript/package-lock.json +++ b/plugins/sdk-typescript/package-lock.json @@ -19,7 +19,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.3.11", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -33,18 +35,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.11", - "@biomejs/cli-darwin-x64": "2.3.11", - "@biomejs/cli-linux-arm64": "2.3.11", - "@biomejs/cli-linux-arm64-musl": "2.3.11", - "@biomejs/cli-linux-x64": "2.3.11", - "@biomejs/cli-linux-x64-musl": "2.3.11", - "@biomejs/cli-win32-arm64": "2.3.11", - "@biomejs/cli-win32-x64": "2.3.11" + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.11", + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", "cpu": [ "arm64" ], @@ -58,8 +62,486 @@ "node": ">=14.21.3" } }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -67,7 +549,75 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" ], "engines": { "node": ">=18" @@ -75,11 +625,43 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -90,8 +672,318 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/chai": { "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -101,16 +993,22 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.7", + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", "dev": true, "license": "MIT", "dependencies": { @@ -119,6 +1017,8 @@ }, "node_modules/@vitest/expect": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { @@ -134,6 +1034,8 @@ }, "node_modules/@vitest/mocker": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -159,6 +1061,8 @@ }, "node_modules/@vitest/pretty-format": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -170,6 +1074,8 @@ }, "node_modules/@vitest/runner": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -183,6 +1089,8 @@ }, "node_modules/@vitest/snapshot": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { @@ -196,6 +1104,8 @@ }, "node_modules/@vitest/spy": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { @@ -207,6 +1117,8 @@ }, "node_modules/@vitest/utils": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { @@ -220,6 +1132,8 @@ }, "node_modules/assertion-error": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -228,6 +1142,8 @@ }, "node_modules/cac": { "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", "engines": { @@ -236,6 +1152,8 @@ }, "node_modules/chai": { "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -251,6 +1169,8 @@ }, "node_modules/check-error": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", "engines": { @@ -259,6 +1179,8 @@ }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -275,6 +1197,8 @@ }, "node_modules/deep-eql": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { @@ -283,11 +1207,15 @@ }, "node_modules/es-module-lexer": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.2", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -298,36 +1226,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -336,6 +1266,8 @@ }, "node_modules/expect-type": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -344,6 +1276,8 @@ }, "node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -360,7 +1294,10 @@ }, "node_modules/fsevents": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -372,16 +1309,22 @@ }, "node_modules/js-tokens": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, "node_modules/loupe": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, "node_modules/magic-string": { "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -390,11 +1333,15 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -412,11 +1359,15 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -425,11 +1376,15 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -441,6 +1396,8 @@ }, "node_modules/postcss": { "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -467,7 +1424,9 @@ } }, "node_modules/rollup": { - "version": "4.57.0", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -481,41 +1440,45 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, "node_modules/siginfo": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -524,16 +1487,22 @@ }, "node_modules/stackback": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, "node_modules/strip-literal": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { @@ -545,16 +1514,22 @@ }, "node_modules/tinybench": { "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -570,6 +1545,8 @@ }, "node_modules/tinypool": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -578,6 +1555,8 @@ }, "node_modules/tinyrainbow": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -586,6 +1565,8 @@ }, "node_modules/tinyspy": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -594,6 +1575,8 @@ }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -606,11 +1589,15 @@ }, "node_modules/undici-types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/vite": { "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { @@ -684,6 +1671,8 @@ }, "node_modules/vite-node": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -705,6 +1694,8 @@ }, "node_modules/vitest": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { @@ -776,6 +1767,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/plugins/sdk-typescript/src/index.ts b/plugins/sdk-typescript/src/index.ts index 72c41e37..61d427c5 100644 --- a/plugins/sdk-typescript/src/index.ts +++ b/plugins/sdk-typescript/src/index.ts @@ -65,16 +65,29 @@ export { // Logger export { createLogger, Logger, type LoggerOptions, type LogLevel } from "./logger.js"; + // Server export { - // Primary exports createMetadataPlugin, - // Deprecated aliases - createSeriesMetadataPlugin, + createRecommendationPlugin, + createSyncPlugin, type InitializeParams, type MetadataPluginOptions, - type SeriesMetadataPluginOptions, + type RecommendationPluginOptions, + type SyncPluginOptions, } from "./server.js"; -// Types +// Storage +export { + PluginStorage, + type StorageClearResponse, + type StorageDeleteResponse, + StorageError, + type StorageGetResponse, + type StorageKeyEntry, + type StorageListResponse, + type StorageSetResponse, +} from "./storage.js"; + +// Types (all types re-exported from barrel) export * from "./types/index.js"; diff --git a/plugins/sdk-typescript/src/server.test.ts b/plugins/sdk-typescript/src/server.test.ts index 2b233c0b..83e3ecae 100644 --- a/plugins/sdk-typescript/src/server.test.ts +++ b/plugins/sdk-typescript/src/server.test.ts @@ -5,6 +5,8 @@ * - Parameter validation for search, get, and match methods * - Error handling for invalid requests * - Request handling flow + * - JSON-RPC response detection (isJsonRpcResponse) + * - Storage response routing in handleLine */ import { describe, expect, it } from "vitest"; @@ -212,3 +214,124 @@ describe("JSON-RPC Error Codes", () => { expect(JSON_RPC_ERROR_CODES.INTERNAL_ERROR).toBe(-32603); }); }); + +// ============================================================================= +// isJsonRpcResponse Tests +// (Mirrors the internal isJsonRpcResponse function in server.ts) +// ============================================================================= + +/** + * Detect whether a parsed JSON object is a JSON-RPC response (not a request). + * This mirrors the internal function in server.ts for testing. + */ +function isJsonRpcResponse(obj: Record): boolean { + if (obj.method !== undefined) return false; + if (obj.id === undefined || obj.id === null) return false; + return "result" in obj || "error" in obj; +} + +describe("isJsonRpcResponse", () => { + it("should detect a success response", () => { + const obj = { jsonrpc: "2.0", id: 1, result: { data: "hello" } }; + expect(isJsonRpcResponse(obj)).toBe(true); + }); + + it("should detect an error response", () => { + const obj = { jsonrpc: "2.0", id: 1, error: { code: -32603, message: "Internal error" } }; + expect(isJsonRpcResponse(obj)).toBe(true); + }); + + it("should detect a response with null result", () => { + const obj = { jsonrpc: "2.0", id: 1, result: null }; + expect(isJsonRpcResponse(obj)).toBe(true); + }); + + it("should detect a response with string id", () => { + const obj = { jsonrpc: "2.0", id: "abc-123", result: "pong" }; + expect(isJsonRpcResponse(obj)).toBe(true); + }); + + it("should detect a response with numeric id 0", () => { + const obj = { jsonrpc: "2.0", id: 0, result: {} }; + expect(isJsonRpcResponse(obj)).toBe(true); + }); + + it("should reject a request (has method field)", () => { + const obj = { jsonrpc: "2.0", id: 1, method: "initialize", params: {} }; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should reject a notification (has method, no id)", () => { + const obj = { jsonrpc: "2.0", method: "shutdown" }; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should reject when id is null", () => { + const obj = { jsonrpc: "2.0", id: null, result: {} }; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should reject when id is undefined (missing)", () => { + const obj = { jsonrpc: "2.0", result: {} }; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should reject an empty object", () => { + const obj = {}; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should reject when object has id but neither result nor error", () => { + const obj = { jsonrpc: "2.0", id: 1 }; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should reject when both method and result are present (treat as request)", () => { + // This should be treated as a request because it has method + const obj = { jsonrpc: "2.0", id: 1, method: "storage/get", result: {} }; + expect(isJsonRpcResponse(obj)).toBe(false); + }); + + it("should accept a response with result: undefined (key present)", () => { + // "result" key is present but value is undefined — still a response shape + const obj: Record = { jsonrpc: "2.0", id: 1 }; + // Explicitly set result key + Object.defineProperty(obj, "result", { value: undefined, enumerable: true }); + expect(isJsonRpcResponse(obj)).toBe(true); + }); +}); + +// ============================================================================= +// Storage Routing in handleLine +// ============================================================================= + +describe("Storage Response Routing", () => { + it("should distinguish a storage response from a host request", () => { + // A storage response: has id + result, no method + const storageResponse = { jsonrpc: "2.0", id: 42, result: { data: "cached_value" } }; + expect(isJsonRpcResponse(storageResponse)).toBe(true); + + // A host request: has method field + const hostRequest = { jsonrpc: "2.0", id: 1, method: "recommendations/get", params: {} }; + expect(isJsonRpcResponse(hostRequest)).toBe(false); + }); + + it("should distinguish a storage error response from a host request", () => { + const storageError = { + jsonrpc: "2.0", + id: 5, + error: { code: -32002, message: "Key not found" }, + }; + expect(isJsonRpcResponse(storageError)).toBe(true); + }); + + it("should not misclassify a parse error response (null id) as a storage response", () => { + // Parse errors have id: null — these should NOT be routed to storage + const parseError = { + jsonrpc: "2.0", + id: null, + error: { code: -32700, message: "Parse error" }, + }; + expect(isJsonRpcResponse(parseError)).toBe(false); + }); +}); diff --git a/plugins/sdk-typescript/src/server.ts b/plugins/sdk-typescript/src/server.ts index 571552f2..6b9fca08 100644 --- a/plugins/sdk-typescript/src/server.ts +++ b/plugins/sdk-typescript/src/server.ts @@ -1,14 +1,25 @@ /** * Plugin server - handles JSON-RPC communication over stdio + * + * Provides factory functions for creating different plugin types. + * All plugin types share a common base server that handles: + * - stdin readline parsing + * - JSON-RPC error handling + * - initialize/ping/shutdown lifecycle methods + * + * Each plugin type adds its own method routing on top. */ import { createInterface } from "node:readline"; import { PluginError } from "./errors.js"; import { createLogger, type Logger } from "./logger.js"; +import { PluginStorage } from "./storage.js"; import type { BookMetadataProvider, MetadataContentType, MetadataProvider, + RecommendationProvider, + SyncProvider, } from "./types/capabilities.js"; import type { PluginManifest } from "./types/manifest.js"; import type { @@ -18,12 +29,13 @@ import type { MetadataMatchParams, MetadataSearchParams, } from "./types/protocol.js"; -import { - JSON_RPC_ERROR_CODES, - type JsonRpcError, - type JsonRpcRequest, - type JsonRpcResponse, -} from "./types/rpc.js"; +import type { + ProfileUpdateRequest, + RecommendationDismissRequest, + RecommendationRequest, +} from "./types/recommendations.js"; +import { JSON_RPC_ERROR_CODES, type JsonRpcRequest, type JsonRpcResponse } from "./types/rpc.js"; +import type { SyncPullRequest, SyncPushRequest } from "./types/sync.js"; // ============================================================================= // Parameter Validation @@ -123,105 +135,69 @@ function invalidParamsError(id: string | number | null, error: ValidationError): code: JSON_RPC_ERROR_CODES.INVALID_PARAMS, message: `Invalid params: ${error.message}`, data: { field: error.field }, - } as JsonRpcError, + }, }; } +// ============================================================================= +// Shared Types +// ============================================================================= + /** * Initialize parameters received from Codex */ export interface InitializeParams { - /** Plugin configuration */ - config?: Record; + /** Admin-level plugin configuration (from plugin settings) */ + adminConfig?: Record; + /** Per-user plugin configuration (from user plugin settings) */ + userConfig?: Record; /** Plugin credentials (API keys, tokens, etc.) */ credentials?: Record; + /** + * Per-user key-value storage client. + * + * Use this to persist data across plugin restarts (e.g., dismissed IDs, + * cached profiles, user preferences). Storage is scoped per user-plugin + * instance — the host resolves the user context automatically. + */ + storage: PluginStorage; } /** - * Options for creating a metadata plugin + * A method router handles capability-specific JSON-RPC methods. + * Returns a response for known methods, or null to indicate "not my method". */ -export interface MetadataPluginOptions { - /** Plugin manifest - must have capabilities.metadataProvider with content types */ - manifest: PluginManifest & { - capabilities: { metadataProvider: MetadataContentType[] }; - }; - /** Series MetadataProvider implementation (required if "series" in metadataProvider) */ - provider?: MetadataProvider; - /** Book MetadataProvider implementation (required if "book" in metadataProvider) */ - bookProvider?: BookMetadataProvider; - /** Called when plugin receives initialize with credentials/config */ - onInitialize?: (params: InitializeParams) => void | Promise; - /** Log level (default: "info") */ - logLevel?: "debug" | "info" | "warn" | "error"; +type MethodRouter = ( + method: string, + params: unknown, + id: string | number | null, +) => Promise; + +// ============================================================================= +// Shared Plugin Server +// ============================================================================= + +interface PluginServerOptions { + manifest: PluginManifest; + onInitialize?: ((params: InitializeParams) => void | Promise) | undefined; + logLevel?: "debug" | "info" | "warn" | "error" | undefined; + label?: string | undefined; + router: MethodRouter; } /** - * Create and run a metadata provider plugin - * - * Creates a plugin server that handles JSON-RPC communication over stdio. - * The TypeScript compiler will ensure you implement all required methods. - * - * @example - * ```typescript - * import { createMetadataPlugin, type MetadataProvider } from "@ashdev/codex-plugin-sdk"; + * Shared plugin server that handles JSON-RPC communication over stdio. * - * const provider: MetadataProvider = { - * async search(params) { - * return { - * results: [{ - * externalId: "123", - * title: "Example", - * alternateTitles: [], - * relevanceScore: 0.95, - * }], - * }; - * }, - * async get(params) { - * return { - * externalId: params.externalId, - * externalUrl: "https://example.com/123", - * alternateTitles: [], - * genres: [], - * tags: [], - * authors: [], - * artists: [], - * externalLinks: [], - * }; - * }, - * }; - * - * createMetadataPlugin({ - * manifest: { - * name: "my-plugin", - * displayName: "My Plugin", - * version: "1.0.0", - * description: "Example plugin", - * author: "Me", - * protocolVersion: "1.0", - * capabilities: { metadataProvider: ["series"] }, - * }, - * provider, - * }); - * ``` + * Handles the common lifecycle methods (initialize, ping, shutdown) and + * delegates capability-specific methods to the provided router. */ -export function createMetadataPlugin(options: MetadataPluginOptions): void { - const { manifest, provider, bookProvider, onInitialize, logLevel = "info" } = options; +function createPluginServer(options: PluginServerOptions): void { + const { manifest, onInitialize, logLevel = "info", label, router } = options; const logger = createLogger({ name: manifest.name, level: logLevel }); + const prefix = label ? `${label} plugin` : "plugin"; + const storage = new PluginStorage(); - // Validate that required providers are present based on manifest - const contentTypes = manifest.capabilities.metadataProvider; - if (contentTypes.includes("series") && !provider) { - throw new Error( - "Series metadata provider is required when 'series' is in metadataProvider capabilities", - ); - } - if (contentTypes.includes("book") && !bookProvider) { - throw new Error( - "Book metadata provider is required when 'book' is in metadataProvider capabilities", - ); - } - - logger.info(`Starting plugin: ${manifest.displayName} v${manifest.version}`); + logger.info(`Starting ${prefix}: ${manifest.displayName} v${manifest.version}`); const rl = createInterface({ input: process.stdin, @@ -229,15 +205,15 @@ export function createMetadataPlugin(options: MetadataPluginOptions): void { }); rl.on("line", (line) => { - void handleLine(line, manifest, provider, bookProvider, onInitialize, logger); + void handleLine(line, manifest, onInitialize, router, logger, storage); }); rl.on("close", () => { logger.info("stdin closed, shutting down"); + storage.cancelAll(); process.exit(0); }); - // Handle uncaught errors process.on("uncaughtException", (error) => { logger.error("Uncaught exception", error); process.exit(1); @@ -248,82 +224,59 @@ export function createMetadataPlugin(options: MetadataPluginOptions): void { }); } -// ============================================================================= -// Backwards Compatibility (deprecated) -// ============================================================================= - -/** - * @deprecated Use createMetadataPlugin instead - */ -export function createSeriesMetadataPlugin(options: SeriesMetadataPluginOptions): void { - // Convert legacy options to new format - const newOptions: MetadataPluginOptions = { - ...options, - manifest: { - ...options.manifest, - capabilities: { - ...options.manifest.capabilities, - metadataProvider: ["series"] as MetadataContentType[], - }, - }, - }; - createMetadataPlugin(newOptions); -} - /** - * @deprecated Use MetadataPluginOptions instead + * Detect whether a parsed JSON object is a JSON-RPC response (not a request). + * + * A response has `id` and either `result` or `error`, but no `method`. + * A request always has `method`. */ -export interface SeriesMetadataPluginOptions { - /** Plugin manifest - must have capabilities.seriesMetadataProvider: true */ - manifest: PluginManifest & { - capabilities: { seriesMetadataProvider: true }; - }; - /** SeriesMetadataProvider implementation */ - provider: MetadataProvider; - /** Called when plugin receives initialize with credentials/config */ - onInitialize?: (params: InitializeParams) => void | Promise; - /** Log level (default: "info") */ - logLevel?: "debug" | "info" | "warn" | "error"; +function isJsonRpcResponse(obj: Record): boolean { + if (obj.method !== undefined) return false; + if (obj.id === undefined || obj.id === null) return false; + return "result" in obj || "error" in obj; } -// ============================================================================= -// Internal Implementation -// ============================================================================= - async function handleLine( line: string, manifest: PluginManifest, - provider: MetadataProvider | undefined, - bookProvider: BookMetadataProvider | undefined, onInitialize: ((params: InitializeParams) => void | Promise) | undefined, + router: MethodRouter, logger: Logger, + storage: PluginStorage, ): Promise { const trimmed = line.trim(); if (!trimmed) return; + // Try to detect storage responses before full request handling. + // Storage responses come from the host on stdin — they have id + (result|error) + // but no method field. + let parsed: Record | undefined; + try { + parsed = JSON.parse(trimmed) as Record; + } catch { + // Will be handled as a parse error below + } + + if (parsed && isJsonRpcResponse(parsed)) { + logger.debug("Routing storage response", { id: parsed.id }); + storage.handleResponse(trimmed); + return; + } + let id: string | number | null = null; try { - const request = JSON.parse(trimmed) as JsonRpcRequest; + const request = (parsed ?? JSON.parse(trimmed)) as JsonRpcRequest; id = request.id; logger.debug(`Received request: ${request.method}`, { id: request.id }); - const response = await handleRequest( - request, - manifest, - provider, - bookProvider, - onInitialize, - logger, - ); - // Shutdown handler writes response directly and returns null + const response = await handleRequest(request, manifest, onInitialize, router, logger, storage); if (response !== null) { writeResponse(response); } } catch (error) { if (error instanceof SyntaxError) { - // JSON parse error writeResponse({ jsonrpc: "2.0", id: null, @@ -356,219 +309,353 @@ async function handleLine( async function handleRequest( request: JsonRpcRequest, manifest: PluginManifest, - provider: MetadataProvider | undefined, - bookProvider: BookMetadataProvider | undefined, onInitialize: ((params: InitializeParams) => void | Promise) | undefined, + router: MethodRouter, logger: Logger, -): Promise { + storage: PluginStorage, +): Promise { const { method, params, id } = request; + // Common lifecycle methods switch (method) { - case "initialize": - // Call onInitialize callback if provided (to receive credentials/config) + case "initialize": { + const initParams = (params ?? {}) as InitializeParams; + // Inject the storage client so plugins can persist data + initParams.storage = storage; if (onInitialize) { - await onInitialize(params as InitializeParams); + await onInitialize(initParams); } - return { - jsonrpc: "2.0", - id, - result: manifest, - }; + return { jsonrpc: "2.0", id, result: manifest }; + } case "ping": - return { - jsonrpc: "2.0", - id, - result: "pong", - }; + return { jsonrpc: "2.0", id, result: "pong" }; case "shutdown": { logger.info("Shutdown requested"); - // Write response directly with callback to ensure it's flushed before exit - const response: JsonRpcResponse = { - jsonrpc: "2.0", - id, - result: null, - }; + storage.cancelAll(); + const response: JsonRpcResponse = { jsonrpc: "2.0", id, result: null }; process.stdout.write(`${JSON.stringify(response)}\n`, () => { - // Callback is called after the write is flushed to the OS process.exit(0); }); - // Return a sentinel that handleLine will recognize and skip normal writeResponse - return null as unknown as JsonRpcResponse; + // Response already written above; return null so handleLine skips the write + return null; } + } - // ========================================================================= - // Series metadata methods - // ========================================================================= - case "metadata/series/search": { - if (!provider) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support series metadata", - }, - }; - } - const validationError = validateSearchParams(params); - if (validationError) { - return invalidParamsError(id, validationError); - } - return { - jsonrpc: "2.0", - id, - result: await provider.search(params as MetadataSearchParams), - }; - } + // Delegate to capability-specific router + const response = await router(method, params, id); + if (response !== null) { + return response; + } - case "metadata/series/get": { - if (!provider) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support series metadata", - }, - }; - } - const validationError = validateGetParams(params); - if (validationError) { - return invalidParamsError(id, validationError); - } - return { - jsonrpc: "2.0", - id, - result: await provider.get(params as MetadataGetParams), - }; - } + // Unknown method + return { + jsonrpc: "2.0", + id, + error: { + code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, + message: `Method not found: ${method}`, + }, + }; +} - case "metadata/series/match": { - if (!provider) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support series metadata", - }, - }; +function writeResponse(response: JsonRpcResponse): void { + process.stdout.write(`${JSON.stringify(response)}\n`); +} + +// ============================================================================= +// Response Helpers +// ============================================================================= + +function methodNotFound(id: string | number | null, message: string): JsonRpcResponse { + return { + jsonrpc: "2.0", + id, + error: { + code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, + message, + }, + }; +} + +function success(id: string | number | null, result: unknown): JsonRpcResponse { + return { jsonrpc: "2.0", id, result }; +} + +// ============================================================================= +// Metadata Plugin +// ============================================================================= + +/** + * Options for creating a metadata plugin + */ +export interface MetadataPluginOptions { + /** Plugin manifest - must have capabilities.metadataProvider with content types */ + manifest: PluginManifest & { + capabilities: { metadataProvider: MetadataContentType[] }; + }; + /** Series MetadataProvider implementation (required if "series" in metadataProvider) */ + provider?: MetadataProvider; + /** Book MetadataProvider implementation (required if "book" in metadataProvider) */ + bookProvider?: BookMetadataProvider; + /** Called when plugin receives initialize with credentials/config */ + onInitialize?: (params: InitializeParams) => void | Promise; + /** Log level (default: "info") */ + logLevel?: "debug" | "info" | "warn" | "error"; +} + +/** + * Create and run a metadata provider plugin + * + * Creates a plugin server that handles JSON-RPC communication over stdio. + * The TypeScript compiler will ensure you implement all required methods. + * + * @example + * ```typescript + * import { createMetadataPlugin, type MetadataProvider } from "@ashdev/codex-plugin-sdk"; + * + * const provider: MetadataProvider = { + * async search(params) { + * return { + * results: [{ + * externalId: "123", + * title: "Example", + * alternateTitles: [], + * relevanceScore: 0.95, + * }], + * }; + * }, + * async get(params) { + * return { + * externalId: params.externalId, + * externalUrl: "https://example.com/123", + * alternateTitles: [], + * genres: [], + * tags: [], + * authors: [], + * artists: [], + * externalLinks: [], + * }; + * }, + * }; + * + * createMetadataPlugin({ + * manifest: { + * name: "my-plugin", + * displayName: "My Plugin", + * version: "1.0.0", + * description: "Example plugin", + * author: "Me", + * protocolVersion: "1.0", + * capabilities: { metadataProvider: ["series"] }, + * }, + * provider, + * }); + * ``` + */ +export function createMetadataPlugin(options: MetadataPluginOptions): void { + const { manifest, provider, bookProvider, onInitialize, logLevel } = options; + + // Validate that required providers are present based on manifest + const contentTypes = manifest.capabilities.metadataProvider; + if (contentTypes.includes("series") && !provider) { + throw new Error( + "Series metadata provider is required when 'series' is in metadataProvider capabilities", + ); + } + if (contentTypes.includes("book") && !bookProvider) { + throw new Error( + "Book metadata provider is required when 'book' is in metadataProvider capabilities", + ); + } + + const router: MethodRouter = async (method, params, id) => { + switch (method) { + // Series metadata methods + case "metadata/series/search": { + if (!provider) return methodNotFound(id, "This plugin does not support series metadata"); + const err = validateSearchParams(params); + if (err) return invalidParamsError(id, err); + return success(id, await provider.search(params as MetadataSearchParams)); } - if (!provider.match) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support series match", - }, - }; + case "metadata/series/get": { + if (!provider) return methodNotFound(id, "This plugin does not support series metadata"); + const err = validateGetParams(params); + if (err) return invalidParamsError(id, err); + return success(id, await provider.get(params as MetadataGetParams)); } - const validationError = validateMatchParams(params); - if (validationError) { - return invalidParamsError(id, validationError); + case "metadata/series/match": { + if (!provider) return methodNotFound(id, "This plugin does not support series metadata"); + if (!provider.match) return methodNotFound(id, "This plugin does not support series match"); + const err = validateMatchParams(params); + if (err) return invalidParamsError(id, err); + return success(id, await provider.match(params as MetadataMatchParams)); } - return { - jsonrpc: "2.0", - id, - result: await provider.match(params as MetadataMatchParams), - }; - } - // ========================================================================= - // Book metadata methods - // ========================================================================= - case "metadata/book/search": { - if (!bookProvider) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support book metadata", - }, - }; + // Book metadata methods + case "metadata/book/search": { + if (!bookProvider) return methodNotFound(id, "This plugin does not support book metadata"); + const err = validateBookSearchParams(params); + if (err) return invalidParamsError(id, err); + return success(id, await bookProvider.search(params as BookSearchParams)); } - const validationError = validateBookSearchParams(params); - if (validationError) { - return invalidParamsError(id, validationError); + case "metadata/book/get": { + if (!bookProvider) return methodNotFound(id, "This plugin does not support book metadata"); + const err = validateGetParams(params); + if (err) return invalidParamsError(id, err); + return success(id, await bookProvider.get(params as MetadataGetParams)); } - return { - jsonrpc: "2.0", - id, - result: await bookProvider.search(params as BookSearchParams), - }; + case "metadata/book/match": { + if (!bookProvider) return methodNotFound(id, "This plugin does not support book metadata"); + if (!bookProvider.match) + return methodNotFound(id, "This plugin does not support book match"); + const err = validateBookMatchParams(params); + if (err) return invalidParamsError(id, err); + return success(id, await bookProvider.match(params as BookMatchParams)); + } + + default: + return null; } + }; - case "metadata/book/get": { - if (!bookProvider) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support book metadata", - }, - }; - } - const validationError = validateGetParams(params); - if (validationError) { - return invalidParamsError(id, validationError); + createPluginServer({ manifest, onInitialize, logLevel, router }); +} + +// ============================================================================= +// Sync Plugin +// ============================================================================= + +/** + * Options for creating a sync provider plugin + */ +export interface SyncPluginOptions { + /** Plugin manifest - must have capabilities.userReadSync: true */ + manifest: PluginManifest & { + capabilities: { userReadSync: true }; + }; + /** SyncProvider implementation */ + provider: SyncProvider; + /** Called when plugin receives initialize with credentials/config */ + onInitialize?: (params: InitializeParams) => void | Promise; + /** Log level (default: "info") */ + logLevel?: "debug" | "info" | "warn" | "error"; +} + +/** + * Create and run a sync provider plugin + * + * Creates a plugin server that handles JSON-RPC communication over stdio + * for sync operations (push/pull reading progress with external services). + * + * @example + * ```typescript + * import { createSyncPlugin, type SyncProvider } from "@ashdev/codex-plugin-sdk"; + * + * const provider: SyncProvider = { + * async getUserInfo() { + * return { externalId: "123", username: "user" }; + * }, + * async pushProgress(params) { + * return { success: [], failed: [] }; + * }, + * async pullProgress(params) { + * return { entries: [], hasMore: false }; + * }, + * }; + * + * createSyncPlugin({ + * manifest: { + * name: "my-sync-plugin", + * displayName: "My Sync Plugin", + * version: "1.0.0", + * description: "Syncs reading progress", + * author: "Me", + * protocolVersion: "1.0", + * capabilities: { userReadSync: true }, + * }, + * provider, + * }); + * ``` + */ +export function createSyncPlugin(options: SyncPluginOptions): void { + const { manifest, provider, onInitialize, logLevel } = options; + + const router: MethodRouter = async (method, params, id) => { + switch (method) { + case "sync/getUserInfo": + return success(id, await provider.getUserInfo()); + case "sync/pushProgress": + return success(id, await provider.pushProgress(params as SyncPushRequest)); + case "sync/pullProgress": + return success(id, await provider.pullProgress(params as SyncPullRequest)); + case "sync/status": { + if (!provider.status) return methodNotFound(id, "This plugin does not support sync/status"); + return success(id, await provider.status()); } - return { - jsonrpc: "2.0", - id, - result: await bookProvider.get(params as MetadataGetParams), - }; + default: + return null; } + }; + + createPluginServer({ manifest, onInitialize, logLevel, label: "sync", router }); +} - case "metadata/book/match": { - if (!bookProvider) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support book metadata", - }, - }; +// ============================================================================= +// Recommendation Plugin +// ============================================================================= + +/** + * Options for creating a recommendation provider plugin + */ +export interface RecommendationPluginOptions { + /** Plugin manifest - must have capabilities.userRecommendationProvider: true */ + manifest: PluginManifest & { + capabilities: { userRecommendationProvider: true }; + }; + /** RecommendationProvider implementation */ + provider: RecommendationProvider; + /** Called when plugin receives initialize with credentials/config */ + onInitialize?: (params: InitializeParams) => void | Promise; + /** Log level (default: "info") */ + logLevel?: "debug" | "info" | "warn" | "error"; +} + +/** + * Create and run a recommendation provider plugin + * + * Creates a plugin server that handles JSON-RPC communication over stdio + * for recommendation operations (get recommendations, update profile, dismiss). + */ +export function createRecommendationPlugin(options: RecommendationPluginOptions): void { + const { manifest, provider, onInitialize, logLevel } = options; + + const router: MethodRouter = async (method, params, id) => { + switch (method) { + case "recommendations/get": + return success(id, await provider.get(params as RecommendationRequest)); + case "recommendations/updateProfile": { + if (!provider.updateProfile) + return methodNotFound(id, "This plugin does not support recommendations/updateProfile"); + return success(id, await provider.updateProfile(params as ProfileUpdateRequest)); } - if (!bookProvider.match) { - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: "This plugin does not support book match", - }, - }; + case "recommendations/clear": { + if (!provider.clear) + return methodNotFound(id, "This plugin does not support recommendations/clear"); + return success(id, await provider.clear()); } - const validationError = validateBookMatchParams(params); - if (validationError) { - return invalidParamsError(id, validationError); + case "recommendations/dismiss": { + if (!provider.dismiss) + return methodNotFound(id, "This plugin does not support recommendations/dismiss"); + const err = validateStringFields(params, ["externalId"]); + if (err) return invalidParamsError(id, err); + return success(id, await provider.dismiss(params as RecommendationDismissRequest)); } - return { - jsonrpc: "2.0", - id, - result: await bookProvider.match(params as BookMatchParams), - }; + default: + return null; } + }; - default: - return { - jsonrpc: "2.0", - id, - error: { - code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND, - message: `Method not found: ${method}`, - }, - }; - } -} - -function writeResponse(response: JsonRpcResponse): void { - // Write to stdout - this is the JSON-RPC channel - process.stdout.write(`${JSON.stringify(response)}\n`); + createPluginServer({ manifest, onInitialize, logLevel, label: "recommendation", router }); } diff --git a/plugins/sdk-typescript/src/storage.test.ts b/plugins/sdk-typescript/src/storage.test.ts new file mode 100644 index 00000000..5f264585 --- /dev/null +++ b/plugins/sdk-typescript/src/storage.test.ts @@ -0,0 +1,442 @@ +/** + * Tests for storage.ts - Plugin storage client + * + * These tests cover: + * - Storage type interfaces and structure + * - StorageError class + * - PluginStorage request building and response handling + * - Full round-trip request/response flow + */ + +import { describe, expect, it } from "vitest"; +import { + PluginStorage, + type StorageClearResponse, + type StorageDeleteResponse, + StorageError, + type StorageGetResponse, + type StorageKeyEntry, + type StorageListResponse, + type StorageSetResponse, +} from "./storage.js"; + +// ============================================================================= +// StorageError Tests +// ============================================================================= + +describe("StorageError", () => { + it("should create error with message and code", () => { + const err = new StorageError("Not found", -32002); + expect(err.message).toBe("Not found"); + expect(err.code).toBe(-32002); + expect(err.data).toBeUndefined(); + expect(err.name).toBe("StorageError"); + }); + + it("should create error with message, code, and data", () => { + const err = new StorageError("Invalid params", -32602, { field: "key" }); + expect(err.message).toBe("Invalid params"); + expect(err.code).toBe(-32602); + expect(err.data).toEqual({ field: "key" }); + }); + + it("should be instanceof Error", () => { + const err = new StorageError("test", -1); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(StorageError); + }); +}); + +// ============================================================================= +// Storage Type Tests (compile-time + runtime shape validation) +// ============================================================================= + +describe("Storage Types", () => { + it("should accept valid StorageGetResponse shape", () => { + const response: StorageGetResponse = { + data: { genres: ["action", "drama"] }, + expiresAt: "2026-12-31T23:59:59Z", + }; + expect(response.data).toEqual({ genres: ["action", "drama"] }); + expect(response.expiresAt).toBe("2026-12-31T23:59:59Z"); + }); + + it("should accept StorageGetResponse with null data", () => { + const response: StorageGetResponse = { data: null }; + expect(response.data).toBeNull(); + }); + + it("should accept valid StorageSetResponse shape", () => { + const response: StorageSetResponse = { success: true }; + expect(response.success).toBe(true); + }); + + it("should accept valid StorageDeleteResponse shape", () => { + const response: StorageDeleteResponse = { deleted: true }; + expect(response.deleted).toBe(true); + + const notDeleted: StorageDeleteResponse = { deleted: false }; + expect(notDeleted.deleted).toBe(false); + }); + + it("should accept valid StorageKeyEntry shape", () => { + const entry: StorageKeyEntry = { + key: "taste_profile", + updatedAt: "2026-02-06T10:00:00Z", + }; + expect(entry.key).toBe("taste_profile"); + expect(entry.updatedAt).toBe("2026-02-06T10:00:00Z"); + }); + + it("should accept StorageKeyEntry with expiresAt", () => { + const entry: StorageKeyEntry = { + key: "cache", + expiresAt: "2026-02-07T00:00:00Z", + updatedAt: "2026-02-06T11:00:00Z", + }; + expect(entry.expiresAt).toBe("2026-02-07T00:00:00Z"); + }); + + it("should accept valid StorageListResponse shape", () => { + const response: StorageListResponse = { + keys: [ + { key: "profile", updatedAt: "2026-02-06T10:00:00Z" }, + { key: "cache", expiresAt: "2026-02-07T00:00:00Z", updatedAt: "2026-02-06T11:00:00Z" }, + ], + }; + expect(response.keys).toHaveLength(2); + expect(response.keys[0].key).toBe("profile"); + expect(response.keys[1].expiresAt).toBe("2026-02-07T00:00:00Z"); + }); + + it("should accept valid StorageClearResponse shape", () => { + const response: StorageClearResponse = { deletedCount: 5 }; + expect(response.deletedCount).toBe(5); + }); +}); + +// ============================================================================= +// Helper: Create a testable storage client with captured writes +// ============================================================================= + +function createTestStorage() { + const written: string[] = []; + const writeFn = (line: string) => { + written.push(line); + }; + const storage = new PluginStorage(writeFn); + return { storage, written }; +} + +/** Simulate a successful JSON-RPC response for a given request ID */ +function successResponse(id: number, result: unknown): string { + return JSON.stringify({ jsonrpc: "2.0", id, result }); +} + +/** Simulate an error JSON-RPC response */ +function errorResponse(id: number, code: number, message: string): string { + return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }); +} + +// ============================================================================= +// PluginStorage Request Building Tests +// ============================================================================= + +describe("PluginStorage - Request Building", () => { + it("should build correct JSON-RPC request for storage/get", () => { + const { storage, written } = createTestStorage(); + + // Start the request (won't resolve yet - no response delivered) + const promise = storage.get("taste_profile"); + + expect(written).toHaveLength(1); + const request = JSON.parse(written[0].trim()); + expect(request.jsonrpc).toBe("2.0"); + expect(request.method).toBe("storage/get"); + expect(request.params).toEqual({ key: "taste_profile" }); + expect(request.id).toBe(1); + + // Deliver response to resolve the promise + storage.handleResponse(successResponse(1, { data: null })); + return promise; // Let vitest verify it resolves + }); + + it("should build correct JSON-RPC request for storage/set", () => { + const { storage, written } = createTestStorage(); + + const promise = storage.set("profile", { version: 1 }); + + const request = JSON.parse(written[0].trim()); + expect(request.method).toBe("storage/set"); + expect(request.params).toEqual({ key: "profile", data: { version: 1 } }); + + storage.handleResponse(successResponse(1, { success: true })); + return promise; + }); + + it("should build correct JSON-RPC request for storage/set with TTL", () => { + const { storage, written } = createTestStorage(); + + const expiresAt = "2026-02-07T00:00:00Z"; + const promise = storage.set("cache", [1, 2, 3], expiresAt); + + const request = JSON.parse(written[0].trim()); + expect(request.method).toBe("storage/set"); + expect(request.params).toEqual({ + key: "cache", + data: [1, 2, 3], + expiresAt: "2026-02-07T00:00:00Z", + }); + + storage.handleResponse(successResponse(1, { success: true })); + return promise; + }); + + it("should not include expiresAt when not provided", () => { + const { storage, written } = createTestStorage(); + + const promise = storage.set("key", "value"); + + const request = JSON.parse(written[0].trim()); + expect(request.params).toEqual({ key: "key", data: "value" }); + expect("expiresAt" in request.params).toBe(false); + + storage.handleResponse(successResponse(1, { success: true })); + return promise; + }); + + it("should build correct JSON-RPC request for storage/delete", () => { + const { storage, written } = createTestStorage(); + + const promise = storage.delete("old_cache"); + + const request = JSON.parse(written[0].trim()); + expect(request.method).toBe("storage/delete"); + expect(request.params).toEqual({ key: "old_cache" }); + + storage.handleResponse(successResponse(1, { deleted: true })); + return promise; + }); + + it("should build correct JSON-RPC request for storage/list", () => { + const { storage, written } = createTestStorage(); + + const promise = storage.list(); + + const request = JSON.parse(written[0].trim()); + expect(request.method).toBe("storage/list"); + expect(request.params).toEqual({}); + + storage.handleResponse(successResponse(1, { keys: [] })); + return promise; + }); + + it("should build correct JSON-RPC request for storage/clear", () => { + const { storage, written } = createTestStorage(); + + const promise = storage.clear(); + + const request = JSON.parse(written[0].trim()); + expect(request.method).toBe("storage/clear"); + expect(request.params).toEqual({}); + + storage.handleResponse(successResponse(1, { deletedCount: 0 })); + return promise; + }); + + it("should increment request IDs", () => { + const { storage, written } = createTestStorage(); + + const p1 = storage.get("key1"); + const p2 = storage.get("key2"); + + const req1 = JSON.parse(written[0].trim()); + const req2 = JSON.parse(written[1].trim()); + + expect(req1.id).toBe(1); + expect(req2.id).toBe(2); + + storage.handleResponse(successResponse(1, { data: null })); + storage.handleResponse(successResponse(2, { data: null })); + return Promise.all([p1, p2]); + }); +}); + +// ============================================================================= +// PluginStorage Response Handling Tests +// ============================================================================= + +describe("PluginStorage - Response Handling", () => { + it("should resolve get request with data", async () => { + const { storage } = createTestStorage(); + + const promise = storage.get("taste_profile"); + storage.handleResponse( + successResponse(1, { data: { genres: ["action"] }, expiresAt: "2026-12-31T23:59:59Z" }), + ); + + const result = await promise; + expect(result.data).toEqual({ genres: ["action"] }); + expect(result.expiresAt).toBe("2026-12-31T23:59:59Z"); + }); + + it("should resolve get request with null data", async () => { + const { storage } = createTestStorage(); + + const promise = storage.get("nonexistent"); + storage.handleResponse(successResponse(1, { data: null })); + + const result = await promise; + expect(result.data).toBeNull(); + }); + + it("should resolve set request", async () => { + const { storage } = createTestStorage(); + + const promise = storage.set("key", { value: 42 }); + storage.handleResponse(successResponse(1, { success: true })); + + const result = await promise; + expect(result.success).toBe(true); + }); + + it("should resolve delete request", async () => { + const { storage } = createTestStorage(); + + const promise = storage.delete("key"); + storage.handleResponse(successResponse(1, { deleted: true })); + + const result = await promise; + expect(result.deleted).toBe(true); + }); + + it("should resolve list request", async () => { + const { storage } = createTestStorage(); + + const promise = storage.list(); + storage.handleResponse( + successResponse(1, { + keys: [ + { key: "profile", updatedAt: "2026-02-06T10:00:00Z" }, + { key: "cache", expiresAt: "2026-02-07T00:00:00Z", updatedAt: "2026-02-06T11:00:00Z" }, + ], + }), + ); + + const result = await promise; + expect(result.keys).toHaveLength(2); + expect(result.keys[0].key).toBe("profile"); + expect(result.keys[1].expiresAt).toBe("2026-02-07T00:00:00Z"); + }); + + it("should resolve clear request", async () => { + const { storage } = createTestStorage(); + + const promise = storage.clear(); + storage.handleResponse(successResponse(1, { deletedCount: 3 })); + + const result = await promise; + expect(result.deletedCount).toBe(3); + }); + + it("should reject on error response", async () => { + const { storage } = createTestStorage(); + + const promise = storage.get("key"); + storage.handleResponse(errorResponse(1, -32603, "Internal error")); + + await expect(promise).rejects.toThrow(StorageError); + await expect(promise).rejects.toThrow("Internal error"); + try { + await promise; + } catch (e) { + expect((e as StorageError).code).toBe(-32603); + } + }); + + it("should handle out-of-order responses", async () => { + const { storage } = createTestStorage(); + + const p1 = storage.get("key1"); + const p2 = storage.get("key2"); + + // Respond to second request first + storage.handleResponse(successResponse(2, { data: "second" })); + storage.handleResponse(successResponse(1, { data: "first" })); + + const r1 = await p1; + const r2 = await p2; + + expect(r1.data).toBe("first"); + expect(r2.data).toBe("second"); + }); + + it("should ignore non-JSON lines", () => { + const { storage } = createTestStorage(); + // Should not throw + storage.handleResponse("not json at all"); + storage.handleResponse(""); + storage.handleResponse(" "); + }); + + it("should ignore host-to-plugin requests (lines with method field)", () => { + const { storage } = createTestStorage(); + const promise = storage.get("key"); + + // This is a host-to-plugin request, not a response - should be ignored + storage.handleResponse( + JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }), + ); + + // Promise should still be pending (not resolved) + storage.cancelAll(); + return expect(promise).rejects.toThrow("Storage client stopped"); + }); + + it("should ignore responses with no matching pending request", () => { + const { storage } = createTestStorage(); + // Should not throw even though there's no pending request with id=999 + storage.handleResponse(successResponse(999, { data: "orphan" })); + }); +}); + +// ============================================================================= +// PluginStorage cancelAll Tests +// ============================================================================= + +describe("PluginStorage - cancelAll", () => { + it("should reject all pending requests", async () => { + const { storage } = createTestStorage(); + + const p1 = storage.get("key1"); + const p2 = storage.set("key2", "value"); + const p3 = storage.delete("key3"); + + storage.cancelAll(); + + await expect(p1).rejects.toThrow("Storage client stopped"); + await expect(p2).rejects.toThrow("Storage client stopped"); + await expect(p3).rejects.toThrow("Storage client stopped"); + }); + + it("should be safe to call with no pending requests", () => { + const { storage } = createTestStorage(); + expect(() => storage.cancelAll()).not.toThrow(); + }); +}); + +// ============================================================================= +// PluginStorage Write Error Tests +// ============================================================================= + +describe("PluginStorage - Write Errors", () => { + it("should reject with StorageError when write throws", async () => { + const storage = new PluginStorage(() => { + throw new Error("pipe broken"); + }); + + const promise = storage.get("key"); + await expect(promise).rejects.toThrow(StorageError); + await expect(promise).rejects.toThrow("pipe broken"); + }); +}); diff --git a/plugins/sdk-typescript/src/storage.ts b/plugins/sdk-typescript/src/storage.ts new file mode 100644 index 00000000..976f2ae7 --- /dev/null +++ b/plugins/sdk-typescript/src/storage.ts @@ -0,0 +1,273 @@ +/** + * Plugin Storage - key-value storage for per-user plugin data + * + * Storage is scoped per user-plugin instance. Plugins only specify a key; + * the host resolves the user context from the connection. + * + * Plugins send storage requests as JSON-RPC calls to the host over stdout + * and receive responses on stdin. This is the reverse of the normal + * host-to-plugin request flow. + * + * @example + * ```typescript + * import { PluginStorage } from "@ashdev/codex-plugin-sdk"; + * + * const storage = new PluginStorage(); + * + * // Store data + * await storage.set("taste_profile", { genres: ["action", "drama"] }); + * + * // Retrieve data + * const data = await storage.get("taste_profile"); + * + * // Store with TTL (expires in 24 hours) + * const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); + * await storage.set("cache", { items: [1, 2, 3] }, expires); + * + * // List all keys + * const keys = await storage.list(); + * + * // Delete a key + * await storage.delete("cache"); + * + * // Clear all data + * await storage.clear(); + * ``` + */ + +import type { JsonRpcError, JsonRpcRequest } from "./types/rpc.js"; + +// ============================================================================= +// Storage Types +// ============================================================================= + +/** Response from storage/get */ +export interface StorageGetResponse { + /** The stored data, or null if key doesn't exist */ + data: unknown | null; + /** Expiration timestamp (ISO 8601) if TTL was set */ + expiresAt?: string; +} + +/** Response from storage/set */ +export interface StorageSetResponse { + /** Always true on success */ + success: boolean; +} + +/** Response from storage/delete */ +export interface StorageDeleteResponse { + /** Whether the key existed and was deleted */ + deleted: boolean; +} + +/** Individual key entry from storage/list */ +export interface StorageKeyEntry { + /** Storage key name */ + key: string; + /** Expiration timestamp (ISO 8601) if TTL was set */ + expiresAt?: string; + /** Last update timestamp (ISO 8601) */ + updatedAt: string; +} + +/** Response from storage/list */ +export interface StorageListResponse { + /** All keys for this plugin instance (excluding expired) */ + keys: StorageKeyEntry[]; +} + +/** Response from storage/clear */ +export interface StorageClearResponse { + /** Number of entries deleted */ + deletedCount: number; +} + +// ============================================================================= +// Storage Error +// ============================================================================= + +/** Error from a storage operation */ +export class StorageError extends Error { + constructor( + message: string, + public readonly code: number, + public readonly data?: unknown, + ) { + super(message); + this.name = "StorageError"; + } +} + +// ============================================================================= +// Plugin Storage Client +// ============================================================================= + +/** Write function signature for sending JSON-RPC requests */ +type WriteFn = (line: string) => void; + +/** + * Client for plugin key-value storage. + * + * Sends JSON-RPC requests to the host process over stdout and reads + * responses on stdin. Each request gets a unique ID so responses can + * be correlated even if they arrive out of order. + */ +export class PluginStorage { + private nextId = 1; + private pendingRequests = new Map< + string | number, + { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + } + >(); + private writeFn: WriteFn; + + /** + * Create a new storage client. + * + * @param writeFn - Optional custom write function (defaults to process.stdout.write). + * Useful for testing or custom transport layers. + */ + constructor(writeFn?: WriteFn) { + this.writeFn = + writeFn ?? + ((line: string) => { + process.stdout.write(line); + }); + } + + /** + * Get a value by key + * + * @param key - Storage key to retrieve + * @returns The stored data and optional expiration, or null data if key doesn't exist + */ + async get(key: string): Promise { + return (await this.sendRequest("storage/get", { key })) as StorageGetResponse; + } + + /** + * Set a value by key (upsert - creates or updates) + * + * @param key - Storage key + * @param data - JSON-serializable data to store + * @param expiresAt - Optional expiration timestamp (ISO 8601) + * @returns Success indicator + */ + async set(key: string, data: unknown, expiresAt?: string): Promise { + const params: Record = { key, data }; + if (expiresAt !== undefined) { + params.expiresAt = expiresAt; + } + return (await this.sendRequest("storage/set", params)) as StorageSetResponse; + } + + /** + * Delete a value by key + * + * @param key - Storage key to delete + * @returns Whether the key existed and was deleted + */ + async delete(key: string): Promise { + return (await this.sendRequest("storage/delete", { key })) as StorageDeleteResponse; + } + + /** + * List all keys for this plugin instance (excluding expired) + * + * @returns List of key entries with metadata + */ + async list(): Promise { + return (await this.sendRequest("storage/list", {})) as StorageListResponse; + } + + /** + * Clear all data for this plugin instance + * + * @returns Number of entries deleted + */ + async clear(): Promise { + return (await this.sendRequest("storage/clear", {})) as StorageClearResponse; + } + + /** + * Handle an incoming JSON-RPC response line from the host. + * + * Call this method from your readline handler to deliver responses + * back to pending storage requests. + */ + handleResponse(line: string): void { + const trimmed = line.trim(); + if (!trimmed) return; + + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch { + // Not JSON - ignore + return; + } + + const obj = parsed as Record; + + // Only handle responses (have "result" or "error", no "method") + if (obj.method !== undefined) { + // This is a host-to-plugin request, not a storage response - ignore + return; + } + + const id = obj.id; + if (id === undefined || id === null) return; + + const pending = this.pendingRequests.get(id as string | number); + if (!pending) return; + + this.pendingRequests.delete(id as string | number); + + if ("error" in obj && obj.error) { + const err = obj.error as JsonRpcError; + pending.reject(new StorageError(err.message, err.code, err.data)); + } else { + pending.resolve(obj.result); + } + } + + /** + * Cancel all pending requests (e.g. on shutdown). + */ + cancelAll(): void { + for (const [, pending] of this.pendingRequests) { + pending.reject(new StorageError("Storage client stopped", -1)); + } + this.pendingRequests.clear(); + } + + // =========================================================================== + // Internal + // =========================================================================== + + private sendRequest(method: string, params: unknown): Promise { + const id = this.nextId++; + + const request: JsonRpcRequest = { + jsonrpc: "2.0", + id, + method, + params, + }; + + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + + try { + this.writeFn(`${JSON.stringify(request)}\n`); + } catch (err) { + this.pendingRequests.delete(id); + const message = err instanceof Error ? err.message : "Unknown write error"; + reject(new StorageError(`Failed to send request: ${message}`, -1)); + } + }); + } +} diff --git a/plugins/sdk-typescript/src/types/capabilities.ts b/plugins/sdk-typescript/src/types/capabilities.ts index 8f208450..41ea3489 100644 --- a/plugins/sdk-typescript/src/types/capabilities.ts +++ b/plugins/sdk-typescript/src/types/capabilities.ts @@ -1,9 +1,8 @@ /** * Capability interfaces - type-safe contracts for plugin capabilities * - * Plugins declare which content types they support in their manifest's - * capabilities.metadataProvider array. The SDK automatically routes - * scoped methods (e.g., metadata/series/search) to the provider. + * All provider interfaces live here. Plugins declare which capabilities + * they support in their manifest, and implement the corresponding interface. * * @example * ```typescript @@ -28,6 +27,23 @@ import type { PluginBookMetadata, PluginSeriesMetadata, } from "./protocol.js"; +import type { + ProfileUpdateRequest, + ProfileUpdateResponse, + RecommendationClearResponse, + RecommendationDismissRequest, + RecommendationDismissResponse, + RecommendationRequest, + RecommendationResponse, +} from "./recommendations.js"; +import type { + ExternalUserInfo, + SyncPullRequest, + SyncPullResponse, + SyncPushRequest, + SyncPushResponse, + SyncStatusResponse, +} from "./sync.js"; // ============================================================================= // Content Types @@ -119,22 +135,106 @@ export interface BookMetadataProvider { } // ============================================================================= -// Future Capabilities (v2) +// Sync Provider Capability // ============================================================================= /** - * Interface for plugins that sync reading progress (syncProvider: true) - * @future v2 - Methods will be defined when sync capability is implemented + * Interface for plugins that sync reading progress. + * + * Plugins implementing this capability can push and pull reading progress + * between Codex and external services (e.g., AniList, MyAnimeList). + * + * Declare this capability in the plugin manifest with `userReadSync: true`. + * + * @example + * ```typescript + * const provider: SyncProvider = { + * async getUserInfo() { + * return { + * externalId: "12345", + * username: "manga_reader", + * avatarUrl: "https://anilist.co/img/avatar.jpg", + * profileUrl: "https://anilist.co/user/manga_reader", + * }; + * }, + * async pushProgress(params) { + * // Push entries to external service + * return { success: [], failed: [] }; + * }, + * async pullProgress(params) { + * // Pull entries from external service + * return { entries: [], hasMore: false }; + * }, + * }; + * ``` */ -// biome-ignore lint/suspicious/noEmptyInterface: Placeholder for future v2 capability -export interface SyncProvider {} +export interface SyncProvider { + /** + * Get user info from the external service. + * + * Returns the user's identity on the external service. + * Used to display the connected account in the UI. + * + * @returns External user information + */ + getUserInfo(): Promise; + + /** + * Push reading progress to the external service. + * + * Sends one or more reading progress entries from Codex to the + * external service. Returns results indicating which entries + * were created, updated, unchanged, or failed. + * + * @param params - Push request with entries to sync + * @returns Push results with success and failure details + */ + pushProgress(params: SyncPushRequest): Promise; + + /** + * Pull reading progress from the external service. + * + * Retrieves reading progress entries from the external service. + * Supports pagination via cursor and incremental sync via `since`. + * + * @param params - Pull request with optional filters and pagination + * @returns Pull results with entries and pagination info + */ + pullProgress(params: SyncPullRequest): Promise; + + /** + * Get sync status overview (optional). + * + * Provides a summary of the sync state between Codex and the + * external service, including pending operations and conflicts. + * + * @returns Sync status information + */ + status?(): Promise; +} + +// ============================================================================= +// Recommendation Provider Capability +// ============================================================================= /** - * Interface for plugins that provide recommendations (recommendationProvider: true) - * @future v2 - Methods will be defined when recommendation capability is implemented + * Interface for plugins that provide recommendations. + * + * Plugins implementing this capability generate personalized suggestions + * based on a user's library and reading history. + * + * Declare this capability in the plugin manifest with `userRecommendationProvider: true`. */ -// biome-ignore lint/suspicious/noEmptyInterface: Placeholder for future v2 capability -export interface RecommendationProvider {} +export interface RecommendationProvider { + /** Get personalized recommendations */ + get(params: RecommendationRequest): Promise; + /** Update the user's taste profile from new activity */ + updateProfile?(params: ProfileUpdateRequest): Promise; + /** Clear cached recommendations */ + clear?(): Promise; + /** Dismiss a recommendation */ + dismiss?(params: RecommendationDismissRequest): Promise; +} // ============================================================================= // Type Helpers @@ -151,17 +251,3 @@ export type PartialMetadataProvider = Partial; * Use this for testing or gradual implementation */ export type PartialBookMetadataProvider = Partial; - -// ============================================================================= -// Backwards Compatibility (deprecated) -// ============================================================================= - -/** - * @deprecated Use MetadataProvider instead - */ -export type SeriesMetadataProvider = MetadataProvider; - -/** - * @deprecated Use PartialMetadataProvider instead - */ -export type PartialSeriesMetadataProvider = PartialMetadataProvider; diff --git a/plugins/sdk-typescript/src/types/index.ts b/plugins/sdk-typescript/src/types/index.ts index eeea5627..dcb99ba6 100644 --- a/plugins/sdk-typescript/src/types/index.ts +++ b/plugins/sdk-typescript/src/types/index.ts @@ -2,18 +2,14 @@ * Re-export all types */ -// From capabilities - interface contracts +// From capabilities - all provider interfaces export type { - // Primary types BookMetadataProvider, MetadataContentType, MetadataProvider, PartialBookMetadataProvider, PartialMetadataProvider, - PartialSeriesMetadataProvider, RecommendationProvider, - // Deprecated aliases - SeriesMetadataProvider, SyncProvider, } from "./capabilities.js"; @@ -22,16 +18,19 @@ export type { ConfigField, ConfigSchema, CredentialField, + OAuthConfig, PluginCapabilities, PluginManifest, } from "./manifest.js"; -export { hasBookMetadataProvider, hasSeriesMetadataProvider } from "./manifest.js"; +export { + EXTERNAL_ID_SOURCE_ANILIST, + hasBookMetadataProvider, + hasSeriesMetadataProvider, +} from "./manifest.js"; -// From protocol - JSON-RPC protocol types (these match Rust exactly) +// From protocol - metadata protocol types (these match Rust exactly) export type { - // Common types AlternateTitle, - // Book metadata types BookAuthor, BookAuthorRole, BookAward, @@ -39,10 +38,10 @@ export type { BookCoverSize, BookMatchParams, BookSearchParams, + ExternalId, ExternalLink, ExternalLinkType, ExternalRating, - // Series metadata types MetadataGetParams, MetadataMatchParams, MetadataMatchResponse, @@ -55,4 +54,32 @@ export type { SearchResultPreview, SeriesStatus, } from "./protocol.js"; +// From recommendations - recommendation protocol types (these match Rust exactly) +export type { + DismissReason, + ProfileUpdateRequest, + ProfileUpdateResponse, + Recommendation, + RecommendationClearResponse, + RecommendationDismissRequest, + RecommendationDismissResponse, + RecommendationRequest, + RecommendationResponse, + UserLibraryEntry, +} from "./recommendations.js"; +// From rpc - JSON-RPC primitives export * from "./rpc.js"; +// From sync - sync protocol types (these match Rust exactly) +export type { + ExternalUserInfo, + SyncEntry, + SyncEntryResult, + SyncEntryResultStatus, + SyncProgress, + SyncPullRequest, + SyncPullResponse, + SyncPushRequest, + SyncPushResponse, + SyncReadingStatus, + SyncStatusResponse, +} from "./sync.js"; diff --git a/plugins/sdk-typescript/src/types/manifest.ts b/plugins/sdk-typescript/src/types/manifest.ts index d16d8bb4..714797ee 100644 --- a/plugins/sdk-typescript/src/types/manifest.ts +++ b/plugins/sdk-typescript/src/types/manifest.ts @@ -33,10 +33,19 @@ export interface PluginCapabilities { * E.g., ["series"] or ["series", "book"] */ metadataProvider?: MetadataContentType[]; - /** Can sync reading progress with external service */ - syncProvider?: boolean; + /** Can sync reading progress with external service (per-user) */ + userReadSync?: boolean; + /** + * External ID source used to match sync entries to Codex series. + * When set, pulled sync entries are matched to series via the + * `series_external_ids` table using this source string. + * + * Should use the `api:` convention, e.g. "api:anilist". + * Only meaningful when `userReadSync` is true. + */ + externalIdSource?: string; /** Can provide recommendations */ - recommendationProvider?: boolean; + userRecommendationProvider?: boolean; } /** @@ -69,6 +78,30 @@ export interface ConfigSchema { fields: ConfigField[]; } +/** + * OAuth 2.0 configuration for user plugins requiring external service authentication. + * + * Codex handles the full OAuth flow (authorization URL, code exchange, token storage). + * Plugins only need to declare their OAuth requirements here. + */ +export interface OAuthConfig { + /** OAuth 2.0 authorization endpoint URL */ + authorizationUrl: string; + /** OAuth 2.0 token endpoint URL */ + tokenUrl: string; + /** Required OAuth scopes */ + scopes?: string[]; + /** + * Whether to use PKCE (Proof Key for Code Exchange). + * Recommended for public clients. Defaults to true. + */ + pkce?: boolean; + /** Optional user info endpoint URL (to fetch external identity after auth) */ + userInfoUrl?: string; + /** Optional default OAuth client ID (can be overridden by admin in plugin config) */ + clientId?: string; +} + /** * Plugin manifest returned by the `initialize` method */ @@ -99,11 +132,45 @@ export interface PluginManifest { /** * Configuration schema documenting available config options. - * This is displayed in the admin UI to help users configure the plugin. + * This is displayed in the admin UI to help administrators configure the plugin. */ configSchema?: ConfigSchema; + + /** + * Configuration schema for per-user settings. + * Displayed in the user-facing Integrations settings modal. + * Users can customize these fields per-account (stored in user_plugins.config). + */ + userConfigSchema?: ConfigSchema; + + /** + * OAuth 2.0 configuration for user plugins that require external service authentication. + * When present, the Integrations UI shows "Connect with {name}" instead of "Enable". + */ + oauth?: OAuthConfig; + + /** User-facing description shown when enabling the plugin */ + userDescription?: string; + + /** Admin-facing setup instructions (e.g., how to create OAuth app, configure client ID) */ + adminSetupInstructions?: string; + + /** User-facing setup instructions (e.g., how to connect or get a personal token) */ + userSetupInstructions?: string; } +// ============================================================================= +// External ID Source Constants +// ============================================================================= + +/** + * Canonical external ID source for AniList. + * + * Use this as the `externalIdSource` in plugin capabilities and when + * creating external ID entries. Follows the `api:` convention. + */ +export const EXTERNAL_ID_SOURCE_ANILIST = "api:anilist" as const; + // ============================================================================= // Type Guards for Manifest Validation // ============================================================================= @@ -131,17 +198,3 @@ export function hasBookMetadataProvider(manifest: PluginManifest): manifest is P manifest.capabilities.metadataProvider.includes("book") ); } - -// ============================================================================= -// Backwards Compatibility (deprecated) -// ============================================================================= - -/** - * @deprecated Use PluginCapabilities with metadataProvider array instead - */ -export interface LegacyPluginCapabilities { - /** @deprecated Use metadataProvider: ["series"] instead */ - seriesMetadataProvider?: boolean; - syncProvider?: boolean; - recommendationProvider?: boolean; -} diff --git a/plugins/sdk-typescript/src/types/protocol.ts b/plugins/sdk-typescript/src/types/protocol.ts index e8a05a5b..462b7ca2 100644 --- a/plugins/sdk-typescript/src/types/protocol.ts +++ b/plugins/sdk-typescript/src/types/protocol.ts @@ -143,6 +143,31 @@ export interface PluginSeriesMetadata { // External links /** Links to other sites */ externalLinks: ExternalLink[]; + + // External IDs (cross-references to other services) + /** + * Cross-reference IDs from other services. + * Uses the `api:` prefix convention (e.g., "api:anilist", "api:myanimelist"). + * + * These allow other plugins (sync, recommendations) to match series + * to external services without needing title-based search. + */ + externalIds?: ExternalId[]; +} + +/** + * Cross-reference ID for a series on an external service. + * + * Source naming convention: + * - `api:` - External API service ID (e.g., "api:anilist", "api:myanimelist") + * - `plugin:` - Plugin match provenance (managed by Codex, not set by plugins) + * - No prefix - File/user sources (e.g., "comicinfo", "epub", "manual") + */ +export interface ExternalId { + /** Source identifier (e.g., "api:anilist", "api:myanimelist", "api:mangadex") */ + source: string; + /** ID on the external service */ + externalId: string; } /** diff --git a/plugins/sdk-typescript/src/types/recommendations.ts b/plugins/sdk-typescript/src/types/recommendations.ts new file mode 100644 index 00000000..7495f302 --- /dev/null +++ b/plugins/sdk-typescript/src/types/recommendations.ts @@ -0,0 +1,159 @@ +/** + * Recommendation Provider Protocol Types + * + * Defines the types for recommendation provider operations. These types MUST match + * the Rust protocol exactly (see src/services/plugin/recommendations.rs in the Codex backend). + * + * Recommendation providers generate personalized suggestions based on a user's + * library and reading history. + * + * ## Methods + * + * - `recommendations/get` - Get personalized recommendations + * - `recommendations/updateProfile` - Update the user's taste profile + * - `recommendations/clear` - Clear cached recommendations + * - `recommendations/dismiss` - Dismiss a recommendation + * + * @see src/services/plugin/recommendations.rs in the Codex backend + */ + +// ============================================================================= +// UserLibraryEntry (matches Rust UserLibraryEntry in protocol.rs) +// ============================================================================= + +/** An entry in the user's library, sent to the plugin for context */ +export interface UserLibraryEntry { + /** Codex series ID */ + seriesId: string; + /** Primary title */ + title: string; + /** Alternate titles */ + alternateTitles: string[]; + /** Year of publication */ + year?: number; + /** Series status (e.g., "ongoing", "completed") */ + status?: string; + /** Genres */ + genres: string[]; + /** Tags */ + tags: string[]; + /** Total number of books in the series */ + totalBookCount?: number; + /** External IDs from metadata providers */ + externalIds: Array<{ source: string; externalId: string }>; + /** User's reading status */ + readingStatus?: string; + /** Number of books the user has read */ + booksRead: number; + /** Number of books the user owns */ + booksOwned: number; + /** User's rating (0-100 scale) */ + userRating?: number; + /** User's notes */ + userNotes?: string; + /** When the user started reading (ISO 8601) */ + startedAt?: string; + /** When the user last read (ISO 8601) */ + lastReadAt?: string; + /** When the user completed reading (ISO 8601) */ + completedAt?: string; +} + +// ============================================================================= +// Recommendation Request/Response +// ============================================================================= + +/** Parameters for `recommendations/get` method */ +export interface RecommendationRequest { + /** User's library entries */ + library: UserLibraryEntry[]; + /** Max recommendations to return */ + limit?: number; + /** External IDs to exclude */ + excludeIds: string[]; +} + +/** A single recommendation */ +export interface Recommendation { + /** External ID on the source service */ + externalId: string; + /** URL to the entry on the external service */ + externalUrl?: string; + /** Title of the recommended series/book */ + title: string; + /** Cover image URL */ + coverUrl?: string; + /** Summary/description */ + summary?: string; + /** Genres */ + genres: string[]; + /** Confidence/relevance score (0.0 to 1.0) */ + score: number; + /** Human-readable reason for this recommendation */ + reason: string; + /** Titles that influenced this recommendation */ + basedOn: string[]; + /** Codex series ID if matched */ + codexSeriesId?: string; + /** Whether this series is already in the user's library */ + inLibrary: boolean; +} + +/** Response from `recommendations/get` method */ +export interface RecommendationResponse { + /** Personalized recommendations */ + recommendations: Recommendation[]; + /** When generated (ISO 8601) */ + generatedAt?: string; + /** Whether cached results */ + cached: boolean; +} + +// ============================================================================= +// Profile Update +// ============================================================================= + +/** Parameters for `recommendations/updateProfile` method */ +export interface ProfileUpdateRequest { + /** Updated library entries */ + entries: UserLibraryEntry[]; +} + +/** Response from `recommendations/updateProfile` method */ +export interface ProfileUpdateResponse { + /** Whether the profile was updated */ + updated: boolean; + /** Number of entries processed */ + entriesProcessed: number; +} + +// ============================================================================= +// Clear +// ============================================================================= + +/** Response from `recommendations/clear` method */ +export interface RecommendationClearResponse { + /** Whether the clear succeeded */ + cleared: boolean; +} + +// ============================================================================= +// Dismiss +// ============================================================================= + +/** Dismiss reason */ +export type DismissReason = "not_interested" | "already_read" | "already_owned"; + +/** Parameters for `recommendations/dismiss` method */ +export interface RecommendationDismissRequest { + /** External ID of the recommendation to dismiss */ + externalId: string; + /** Reason for dismissal */ + reason?: DismissReason; +} + +/** Response from `recommendations/dismiss` method */ +export interface RecommendationDismissResponse { + /** Whether the dismissal was recorded */ + dismissed: boolean; +} diff --git a/plugins/sdk-typescript/src/types/sync.test.ts b/plugins/sdk-typescript/src/types/sync.test.ts new file mode 100644 index 00000000..c4e66751 --- /dev/null +++ b/plugins/sdk-typescript/src/types/sync.test.ts @@ -0,0 +1,637 @@ +/** + * Tests for sync protocol types and SyncProvider interface + * + * These tests cover: + * - Type definitions compile correctly with valid data + * - All enum string literal values match Rust snake_case serialization + * - Interface shapes match the Rust protocol (camelCase properties) + * - Optional fields are properly handled + * - SyncProvider interface method signatures + */ + +import { describe, expect, it } from "vitest"; +import type { SyncProvider } from "./capabilities.js"; +import type { + ExternalUserInfo, + SyncEntry, + SyncEntryResult, + SyncEntryResultStatus, + SyncProgress, + SyncPullRequest, + SyncPullResponse, + SyncPushRequest, + SyncPushResponse, + SyncReadingStatus, + SyncStatusResponse, +} from "./sync.js"; + +// ============================================================================= +// SyncReadingStatus Tests +// ============================================================================= + +describe("SyncReadingStatus", () => { + it("should accept all valid reading status values", () => { + const statuses: SyncReadingStatus[] = [ + "reading", + "completed", + "on_hold", + "dropped", + "plan_to_read", + ]; + expect(statuses).toHaveLength(5); + expect(statuses).toContain("reading"); + expect(statuses).toContain("completed"); + expect(statuses).toContain("on_hold"); + expect(statuses).toContain("dropped"); + expect(statuses).toContain("plan_to_read"); + }); + + it("should use snake_case values matching Rust serialization", () => { + // These must match the Rust #[serde(rename_all = "snake_case")] output + const onHold: SyncReadingStatus = "on_hold"; + const planToRead: SyncReadingStatus = "plan_to_read"; + expect(onHold).toBe("on_hold"); + expect(planToRead).toBe("plan_to_read"); + }); +}); + +// ============================================================================= +// SyncEntryResultStatus Tests +// ============================================================================= + +describe("SyncEntryResultStatus", () => { + it("should accept all valid result status values", () => { + const statuses: SyncEntryResultStatus[] = ["created", "updated", "unchanged", "failed"]; + expect(statuses).toHaveLength(4); + expect(statuses).toContain("created"); + expect(statuses).toContain("updated"); + expect(statuses).toContain("unchanged"); + expect(statuses).toContain("failed"); + }); +}); + +// ============================================================================= +// ExternalUserInfo Tests +// ============================================================================= + +describe("ExternalUserInfo", () => { + it("should accept full user info with all fields", () => { + const info: ExternalUserInfo = { + externalId: "12345", + username: "manga_reader", + avatarUrl: "https://anilist.co/img/avatar.jpg", + profileUrl: "https://anilist.co/user/manga_reader", + }; + expect(info.externalId).toBe("12345"); + expect(info.username).toBe("manga_reader"); + expect(info.avatarUrl).toBe("https://anilist.co/img/avatar.jpg"); + expect(info.profileUrl).toBe("https://anilist.co/user/manga_reader"); + }); + + it("should accept minimal user info without optional fields", () => { + const info: ExternalUserInfo = { + externalId: "99", + username: "user99", + }; + expect(info.externalId).toBe("99"); + expect(info.username).toBe("user99"); + expect(info.avatarUrl).toBeUndefined(); + expect(info.profileUrl).toBeUndefined(); + }); + + it("should use camelCase property names matching Rust serialization", () => { + const info: ExternalUserInfo = { + externalId: "1", + username: "test", + avatarUrl: "https://example.com/avatar.jpg", + profileUrl: "https://example.com/profile", + }; + // Verify camelCase keys exist (matching serde rename_all = "camelCase") + expect("externalId" in info).toBe(true); + expect("avatarUrl" in info).toBe(true); + expect("profileUrl" in info).toBe(true); + }); +}); + +// ============================================================================= +// SyncProgress Tests +// ============================================================================= + +describe("SyncProgress", () => { + it("should accept full progress with all fields", () => { + const progress: SyncProgress = { + chapters: 100, + volumes: 10, + pages: 3200, + }; + expect(progress.chapters).toBe(100); + expect(progress.volumes).toBe(10); + expect(progress.pages).toBe(3200); + }); + + it("should accept partial progress with only chapters", () => { + const progress: SyncProgress = { + chapters: 50, + }; + expect(progress.chapters).toBe(50); + expect(progress.volumes).toBeUndefined(); + expect(progress.pages).toBeUndefined(); + }); + + it("should accept empty progress with no fields", () => { + const progress: SyncProgress = {}; + expect(progress.chapters).toBeUndefined(); + expect(progress.volumes).toBeUndefined(); + expect(progress.pages).toBeUndefined(); + }); +}); + +// ============================================================================= +// SyncEntry Tests +// ============================================================================= + +describe("SyncEntry", () => { + it("should accept full entry with all fields", () => { + const entry: SyncEntry = { + externalId: "12345", + status: "reading", + progress: { + chapters: 42, + volumes: 5, + }, + score: 8.5, + startedAt: "2026-01-15T00:00:00Z", + completedAt: "2026-02-01T00:00:00Z", + notes: "Great series!", + }; + expect(entry.externalId).toBe("12345"); + expect(entry.status).toBe("reading"); + expect(entry.progress?.chapters).toBe(42); + expect(entry.progress?.volumes).toBe(5); + expect(entry.score).toBe(8.5); + expect(entry.startedAt).toBe("2026-01-15T00:00:00Z"); + expect(entry.completedAt).toBe("2026-02-01T00:00:00Z"); + expect(entry.notes).toBe("Great series!"); + }); + + it("should accept minimal entry with only required fields", () => { + const entry: SyncEntry = { + externalId: "99", + status: "completed", + }; + expect(entry.externalId).toBe("99"); + expect(entry.status).toBe("completed"); + expect(entry.progress).toBeUndefined(); + expect(entry.score).toBeUndefined(); + expect(entry.startedAt).toBeUndefined(); + expect(entry.completedAt).toBeUndefined(); + expect(entry.notes).toBeUndefined(); + }); + + it("should accept all reading statuses in entries", () => { + const statuses: SyncReadingStatus[] = [ + "reading", + "completed", + "on_hold", + "dropped", + "plan_to_read", + ]; + for (const status of statuses) { + const entry: SyncEntry = { externalId: "1", status }; + expect(entry.status).toBe(status); + } + }); + + it("should use camelCase property names matching Rust serialization", () => { + const entry: SyncEntry = { + externalId: "1", + status: "reading", + startedAt: "2026-01-01T00:00:00Z", + completedAt: "2026-02-01T00:00:00Z", + }; + expect("externalId" in entry).toBe(true); + expect("startedAt" in entry).toBe(true); + expect("completedAt" in entry).toBe(true); + }); +}); + +// ============================================================================= +// SyncPushRequest Tests +// ============================================================================= + +describe("SyncPushRequest", () => { + it("should accept request with multiple entries", () => { + const request: SyncPushRequest = { + entries: [ + { + externalId: "1", + status: "reading", + progress: { chapters: 10 }, + }, + { + externalId: "2", + status: "completed", + score: 9.0, + completedAt: "2026-02-01T00:00:00Z", + }, + ], + }; + expect(request.entries).toHaveLength(2); + expect(request.entries[0]?.externalId).toBe("1"); + expect(request.entries[1]?.status).toBe("completed"); + }); + + it("should accept request with empty entries array", () => { + const request: SyncPushRequest = { entries: [] }; + expect(request.entries).toHaveLength(0); + }); +}); + +// ============================================================================= +// SyncEntryResult Tests +// ============================================================================= + +describe("SyncEntryResult", () => { + it("should accept successful result without error", () => { + const result: SyncEntryResult = { + externalId: "1", + status: "updated", + }; + expect(result.externalId).toBe("1"); + expect(result.status).toBe("updated"); + expect(result.error).toBeUndefined(); + }); + + it("should accept failed result with error message", () => { + const result: SyncEntryResult = { + externalId: "3", + status: "failed", + error: "Rate limited", + }; + expect(result.externalId).toBe("3"); + expect(result.status).toBe("failed"); + expect(result.error).toBe("Rate limited"); + }); + + it("should accept all result status values", () => { + const statuses: SyncEntryResultStatus[] = ["created", "updated", "unchanged", "failed"]; + for (const status of statuses) { + const result: SyncEntryResult = { externalId: "1", status }; + expect(result.status).toBe(status); + } + }); +}); + +// ============================================================================= +// SyncPushResponse Tests +// ============================================================================= + +describe("SyncPushResponse", () => { + it("should accept response with success and failed entries", () => { + const response: SyncPushResponse = { + success: [ + { externalId: "1", status: "updated" }, + { externalId: "2", status: "created" }, + ], + failed: [{ externalId: "3", status: "failed", error: "Rate limited" }], + }; + expect(response.success).toHaveLength(2); + expect(response.success[0]?.status).toBe("updated"); + expect(response.success[1]?.status).toBe("created"); + expect(response.failed).toHaveLength(1); + expect(response.failed[0]?.status).toBe("failed"); + expect(response.failed[0]?.error).toBe("Rate limited"); + }); + + it("should accept response with all success and no failures", () => { + const response: SyncPushResponse = { + success: [{ externalId: "1", status: "unchanged" }], + failed: [], + }; + expect(response.success).toHaveLength(1); + expect(response.failed).toHaveLength(0); + }); + + it("should accept empty response", () => { + const response: SyncPushResponse = { + success: [], + failed: [], + }; + expect(response.success).toHaveLength(0); + expect(response.failed).toHaveLength(0); + }); +}); + +// ============================================================================= +// SyncPullRequest Tests +// ============================================================================= + +describe("SyncPullRequest", () => { + it("should accept request with all fields", () => { + const request: SyncPullRequest = { + since: "2026-02-01T00:00:00Z", + limit: 50, + cursor: "next_page_token", + }; + expect(request.since).toBe("2026-02-01T00:00:00Z"); + expect(request.limit).toBe(50); + expect(request.cursor).toBe("next_page_token"); + }); + + it("should accept empty request with no fields", () => { + const request: SyncPullRequest = {}; + expect(request.since).toBeUndefined(); + expect(request.limit).toBeUndefined(); + expect(request.cursor).toBeUndefined(); + }); + + it("should accept request with only since", () => { + const request: SyncPullRequest = { + since: "2026-01-01T00:00:00Z", + }; + expect(request.since).toBe("2026-01-01T00:00:00Z"); + }); + + it("should accept request with only cursor for pagination", () => { + const request: SyncPullRequest = { + cursor: "page_2_cursor", + }; + expect(request.cursor).toBe("page_2_cursor"); + }); +}); + +// ============================================================================= +// SyncPullResponse Tests +// ============================================================================= + +describe("SyncPullResponse", () => { + it("should accept response with entries and pagination", () => { + const response: SyncPullResponse = { + entries: [ + { + externalId: "42", + status: "on_hold", + progress: { chapters: 25 }, + score: 7.0, + }, + ], + nextCursor: "page2", + hasMore: true, + }; + expect(response.entries).toHaveLength(1); + expect(response.entries[0]?.status).toBe("on_hold"); + expect(response.nextCursor).toBe("page2"); + expect(response.hasMore).toBe(true); + }); + + it("should accept last page response with no more entries", () => { + const response: SyncPullResponse = { + entries: [], + hasMore: false, + }; + expect(response.entries).toHaveLength(0); + expect(response.nextCursor).toBeUndefined(); + expect(response.hasMore).toBe(false); + }); + + it("should use camelCase for nextCursor and hasMore", () => { + const response: SyncPullResponse = { + entries: [], + nextCursor: "abc", + hasMore: true, + }; + expect("nextCursor" in response).toBe(true); + expect("hasMore" in response).toBe(true); + }); +}); + +// ============================================================================= +// SyncStatusResponse Tests +// ============================================================================= + +describe("SyncStatusResponse", () => { + it("should accept full status response", () => { + const response: SyncStatusResponse = { + lastSyncAt: "2026-02-06T12:00:00Z", + externalCount: 150, + pendingPush: 5, + pendingPull: 3, + conflicts: 1, + }; + expect(response.lastSyncAt).toBe("2026-02-06T12:00:00Z"); + expect(response.externalCount).toBe(150); + expect(response.pendingPush).toBe(5); + expect(response.pendingPull).toBe(3); + expect(response.conflicts).toBe(1); + }); + + it("should accept minimal status response with required fields only", () => { + const response: SyncStatusResponse = { + pendingPush: 0, + pendingPull: 0, + conflicts: 0, + }; + expect(response.lastSyncAt).toBeUndefined(); + expect(response.externalCount).toBeUndefined(); + expect(response.pendingPush).toBe(0); + expect(response.pendingPull).toBe(0); + expect(response.conflicts).toBe(0); + }); + + it("should use camelCase property names matching Rust serialization", () => { + const response: SyncStatusResponse = { + lastSyncAt: "2026-02-06T12:00:00Z", + externalCount: 100, + pendingPush: 0, + pendingPull: 0, + conflicts: 0, + }; + expect("lastSyncAt" in response).toBe(true); + expect("externalCount" in response).toBe(true); + expect("pendingPush" in response).toBe(true); + expect("pendingPull" in response).toBe(true); + }); +}); + +// ============================================================================= +// SyncProvider Interface Tests +// ============================================================================= + +describe("SyncProvider", () => { + it("should accept a complete sync provider implementation", () => { + const provider: SyncProvider = { + async getUserInfo(): Promise { + return { + externalId: "12345", + username: "manga_reader", + avatarUrl: "https://anilist.co/img/avatar.jpg", + profileUrl: "https://anilist.co/user/manga_reader", + }; + }, + async pushProgress(params: SyncPushRequest): Promise { + return { + success: params.entries.map((e) => ({ + externalId: e.externalId, + status: "updated" as SyncEntryResultStatus, + })), + failed: [], + }; + }, + async pullProgress(_params: SyncPullRequest): Promise { + return { + entries: [ + { + externalId: "42", + status: "reading", + progress: { chapters: 10 }, + }, + ], + hasMore: false, + }; + }, + async status(): Promise { + return { + lastSyncAt: "2026-02-06T12:00:00Z", + externalCount: 42, + pendingPush: 0, + pendingPull: 0, + conflicts: 0, + }; + }, + }; + + expect(provider.getUserInfo).toBeDefined(); + expect(provider.pushProgress).toBeDefined(); + expect(provider.pullProgress).toBeDefined(); + expect(provider.status).toBeDefined(); + }); + + it("should accept provider without optional status method", () => { + const provider: SyncProvider = { + async getUserInfo(): Promise { + return { externalId: "1", username: "test" }; + }, + async pushProgress(_params: SyncPushRequest): Promise { + return { success: [], failed: [] }; + }, + async pullProgress(_params: SyncPullRequest): Promise { + return { entries: [], hasMore: false }; + }, + }; + + expect(provider.getUserInfo).toBeDefined(); + expect(provider.pushProgress).toBeDefined(); + expect(provider.pullProgress).toBeDefined(); + expect(provider.status).toBeUndefined(); + }); + + it("should produce correct return types from provider methods", async () => { + const provider: SyncProvider = { + async getUserInfo() { + return { externalId: "1", username: "test" }; + }, + async pushProgress() { + return { success: [], failed: [] }; + }, + async pullProgress() { + return { entries: [], hasMore: false }; + }, + }; + + const userInfo = await provider.getUserInfo(); + expect(userInfo.externalId).toBe("1"); + expect(userInfo.username).toBe("test"); + + const pushResult = await provider.pushProgress({ entries: [] }); + expect(pushResult.success).toEqual([]); + expect(pushResult.failed).toEqual([]); + + const pullResult = await provider.pullProgress({}); + expect(pullResult.entries).toEqual([]); + expect(pullResult.hasMore).toBe(false); + }); +}); + +// ============================================================================= +// Cross-type Integration Tests +// ============================================================================= + +describe("Sync Protocol Integration", () => { + it("should round-trip a full push flow with correct types", () => { + // Build a push request + const request: SyncPushRequest = { + entries: [ + { + externalId: "anilist:12345", + status: "reading", + progress: { chapters: 42, volumes: 5 }, + score: 8.5, + startedAt: "2026-01-15T00:00:00Z", + notes: "Enjoying this series", + }, + { + externalId: "anilist:67890", + status: "completed", + progress: { chapters: 200, volumes: 20 }, + score: 9.5, + startedAt: "2025-06-01T00:00:00Z", + completedAt: "2026-01-30T00:00:00Z", + }, + { + externalId: "anilist:11111", + status: "plan_to_read", + }, + ], + }; + + // Build a push response + const response: SyncPushResponse = { + success: [ + { externalId: "anilist:12345", status: "updated" }, + { externalId: "anilist:67890", status: "unchanged" }, + { externalId: "anilist:11111", status: "created" }, + ], + failed: [], + }; + + expect(request.entries).toHaveLength(3); + expect(response.success).toHaveLength(3); + expect(response.failed).toHaveLength(0); + }); + + it("should round-trip a full pull flow with pagination", () => { + // First page request + const request1: SyncPullRequest = { + since: "2026-02-01T00:00:00Z", + limit: 2, + }; + + // First page response + const response1: SyncPullResponse = { + entries: [ + { externalId: "1", status: "reading", progress: { chapters: 10 } }, + { externalId: "2", status: "completed", score: 9.0 }, + ], + nextCursor: "cursor_page_2", + hasMore: true, + }; + + // Second page request (using cursor) + const request2: SyncPullRequest = { + cursor: response1.nextCursor, + limit: 2, + }; + + // Second (last) page response + const response2: SyncPullResponse = { + entries: [{ externalId: "3", status: "dropped" }], + hasMore: false, + }; + + expect(request1.since).toBe("2026-02-01T00:00:00Z"); + expect(response1.hasMore).toBe(true); + expect(request2.cursor).toBe("cursor_page_2"); + expect(response2.hasMore).toBe(false); + expect(response2.nextCursor).toBeUndefined(); + }); +}); diff --git a/plugins/sdk-typescript/src/types/sync.ts b/plugins/sdk-typescript/src/types/sync.ts new file mode 100644 index 00000000..4d348b62 --- /dev/null +++ b/plugins/sdk-typescript/src/types/sync.ts @@ -0,0 +1,217 @@ +/** + * Sync Provider Protocol Types + * + * Defines the types for sync provider operations. These types MUST match + * the Rust protocol exactly (see src/services/plugin/sync.rs in the Codex backend). + * + * Sync providers push and pull reading progress between Codex and external + * services like AniList and MyAnimeList. + * + * ## Architecture + * + * Sync operations are initiated by the host (Codex) and sent to the plugin. + * The plugin communicates with the external service using user credentials + * provided during initialization. + * + * ## Methods + * + * - `sync/getUserInfo` - Get user info from external service + * - `sync/pushProgress` - Push reading progress to external service + * - `sync/pullProgress` - Pull reading progress from external service + * - `sync/status` - Get sync status/diff between Codex and external + * + * @see src/services/plugin/sync.rs in the Codex backend + */ + +// ============================================================================= +// Reading Status +// ============================================================================= + +/** + * Reading status for sync entries. + * + * Uses snake_case values to match Rust's `#[serde(rename_all = "snake_case")]`. + */ +export type SyncReadingStatus = "reading" | "completed" | "on_hold" | "dropped" | "plan_to_read"; + +// ============================================================================= +// Sync Entry Result Status +// ============================================================================= + +/** + * Status of a single sync entry operation result. + * + * Uses snake_case values to match Rust's `#[serde(rename_all = "snake_case")]`. + */ +export type SyncEntryResultStatus = "created" | "updated" | "unchanged" | "failed"; + +// ============================================================================= +// User Info +// ============================================================================= + +/** + * Response from `sync/getUserInfo` method. + * + * Returns the user's identity on the external service. + * Used to display the connected account in the UI. + */ +export interface ExternalUserInfo { + /** User ID on the external service */ + externalId: string; + /** Display name / username */ + username: string; + /** Avatar/profile image URL */ + avatarUrl?: string; + /** Profile URL on the external service */ + profileUrl?: string; +} + +// ============================================================================= +// Sync Progress +// ============================================================================= + +/** + * Reading progress details. + * + * All fields are optional to support different tracking granularities + * (e.g., chapter-based for manga, page-based for single volumes). + */ +export interface SyncProgress { + /** Number of chapters read */ + chapters?: number; + /** Number of volumes read */ + volumes?: number; + /** Number of pages read (for single-volume works) */ + pages?: number; + /** Total number of chapters in the series (if known) */ + totalChapters?: number; + /** Total number of volumes in the series (if known) */ + totalVolumes?: number; +} + +// ============================================================================= +// Sync Entry (shared between push and pull) +// ============================================================================= + +/** + * A single reading progress entry for sync. + * + * Represents one series/book's reading state that can be pushed to + * or pulled from an external service. + */ +export interface SyncEntry { + /** External ID on the target service (e.g., AniList media ID) */ + externalId: string; + /** Reading status */ + status: SyncReadingStatus; + /** Reading progress */ + progress?: SyncProgress; + /** User's score/rating (service-specific scale, e.g., 1-10 or 1-100) */ + score?: number; + /** When the user started reading (ISO 8601) */ + startedAt?: string; + /** When the user completed reading (ISO 8601) */ + completedAt?: string; + /** User notes */ + notes?: string; + /** + * When the series was most recently updated (ISO 8601). + * Populated from the most recent read_progress.updated_at for the series. + * Plugins can use this for time-based logic (e.g., pause/drop stale series). + */ + latestUpdatedAt?: string; + /** + * Series title (for plugins that support title-based search fallback). + * Populated when the backend knows the series name. Plugins can use this + * to search the external service by title when no external ID is present. + */ + title?: string; +} + +// ============================================================================= +// Push Progress +// ============================================================================= + +/** + * Parameters for `sync/pushProgress` method. + * + * Sends reading progress from Codex to the external service. + */ +export interface SyncPushRequest { + /** Entries to push to the external service */ + entries: SyncEntry[]; +} + +/** + * Result for a single sync entry (push or pull). + */ +export interface SyncEntryResult { + /** External ID of the entry */ + externalId: string; + /** Result status */ + status: SyncEntryResultStatus; + /** Error message if failed */ + error?: string; +} + +/** + * Response from `sync/pushProgress` method. + */ +export interface SyncPushResponse { + /** Successfully synced entries */ + success: SyncEntryResult[]; + /** Failed entries */ + failed: SyncEntryResult[]; +} + +// ============================================================================= +// Pull Progress +// ============================================================================= + +/** + * Parameters for `sync/pullProgress` method. + * + * Requests reading progress from the external service. + */ +export interface SyncPullRequest { + /** Only pull entries updated after this timestamp (ISO 8601). If not set, pulls all entries. */ + since?: string; + /** Maximum number of entries to pull */ + limit?: number; + /** Pagination cursor for continuing a previous pull */ + cursor?: string; +} + +/** + * Response from `sync/pullProgress` method. + */ +export interface SyncPullResponse { + /** Entries pulled from the external service */ + entries: SyncEntry[]; + /** Cursor for next page (if more entries available) */ + nextCursor?: string; + /** Whether there are more entries to pull */ + hasMore: boolean; +} + +// ============================================================================= +// Sync Status +// ============================================================================= + +/** + * Response from `sync/status` method. + * + * Provides an overview of the sync state between Codex and the external service. + */ +export interface SyncStatusResponse { + /** Last successful sync timestamp (ISO 8601) */ + lastSyncAt?: string; + /** Number of entries on the external service */ + externalCount?: number; + /** Number of entries that need to be pushed */ + pendingPush: number; + /** Number of entries that need to be pulled */ + pendingPull: number; + /** Entries with conflicts (different on both sides) */ + conflicts: number; +} diff --git a/plugins/sync-anilist/.gitignore b/plugins/sync-anilist/.gitignore new file mode 100644 index 00000000..f4e2c6d6 --- /dev/null +++ b/plugins/sync-anilist/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/plugins/sync-anilist/README.md b/plugins/sync-anilist/README.md new file mode 100644 index 00000000..28584ff5 --- /dev/null +++ b/plugins/sync-anilist/README.md @@ -0,0 +1,218 @@ +# @ashdev/codex-plugin-sync-anilist + +A Codex plugin for syncing manga reading progress between Codex and [AniList](https://anilist.co). Supports push/pull of reading status, volumes/chapters read, scores, dates, and notes. + +## Features + +- Two-way sync of manga reading progress with AniList +- Push reading status, progress, scores, dates, and notes to AniList +- Pull updates from AniList back to Codex +- Configurable progress unit (volumes or chapters) +- Auto-pause/auto-drop stale series based on reading inactivity +- Rate limit handling with automatic retry +- Highest-progress-wins conflict resolution (progress only moves forward) +- External ID matching via AniList API IDs (`api:anilist`) + +## Authentication + +This plugin supports two authentication methods: + +### OAuth (Recommended) + +If your Codex administrator has configured OAuth: + +1. Go to **Settings** > **Integrations** +2. Click **Connect with AniList Sync** +3. Authorize Codex on AniList +4. You're connected! + +### Personal Access Token + +If OAuth is not configured by the admin: + +1. Go to [AniList Developer Settings](https://anilist.co/settings/developer) +2. Click **Create New Client** +3. Set the redirect URL to `https://anilist.co/api/v2/oauth/pin` +4. Click **Save**, then **Authorize** your new client +5. Copy the token shown on the pin page +6. In Codex, go to **Settings** > **Integrations** +7. Paste the token in the access token field and click **Save Token** + +## Admin Setup + +### Adding the Plugin to Codex + +1. Log in to Codex as an administrator +2. Navigate to **Settings** > **Plugins** +3. Click **Add Plugin** +4. Fill in the form: + - **Name**: `sync-anilist` + - **Display Name**: `AniList Sync` + - **Command**: `npx` + - **Arguments**: `-y @ashdev/codex-plugin-sync-anilist@1.9.3` +5. Click **Save** +6. Click **Test Connection** to verify the plugin works + +### Configuring OAuth (Optional) + +To enable OAuth login for your users: + +1. Go to [AniList Developer Settings](https://anilist.co/settings/developer) +2. Click **Create New Client** +3. Set the redirect URL to `{your-codex-url}/api/v1/user/plugins/oauth/callback` +4. Save and copy the **Client ID** +5. In Codex, go to **Settings** > **Plugins** > click the gear icon on AniList Sync +6. Go to the **OAuth** tab +7. Paste the **Client ID** (and optionally the **Client Secret**) +8. Click **Save Changes** + +Without OAuth configured, users can still connect by pasting a personal access token. + +### npx Options + +| Configuration | Arguments | Description | +|--------------|-----------|-------------| +| Latest version | `-y @ashdev/codex-plugin-sync-anilist` | Always uses latest | +| Pinned version | `-y @ashdev/codex-plugin-sync-anilist@1.9.3` | Recommended for production | +| Fast startup | `-y --prefer-offline @ashdev/codex-plugin-sync-anilist@1.9.3` | Skips version check if cached | + +## Configuration + +### Codex Sync Settings + +These settings are managed in the **Sync Settings** section of the plugin settings modal. They control which data the Codex server sends to the plugin and are shared across all sync plugins. + +| Setting | Default | Description | +|---------|---------|-------------| +| Include completed series | On | Include series where all local books are read | +| Include in-progress series | On | Include series where at least one book has been started | +| Count partially-read books | Off | Whether partially-read books count toward the progress number | +| Sync ratings & notes | On | Include user ratings and notes in sync | + +### Plugin-Specific Settings + +These settings are specific to the AniList plugin and appear under **Plugin Settings** in the settings modal. + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| Progress Unit | string | `volumes` | Whether each Codex book counts as a "volume" or "chapter" on AniList. Use "volumes" to avoid misleading "Read chapter X" activity on AniList. | +| Auto-Pause After Days | number | `0` (disabled) | Number of days without reading activity before an in-progress series is set to Paused on AniList | +| Auto-Drop After Days | number | `0` (disabled) | Number of days without reading activity before an in-progress series is set to Dropped on AniList | + +## Using the Plugin + +Once connected, the sync plugin works automatically: + +1. Go to **Settings** > **Integrations** +2. Click **Sync Now** to trigger a manual sync +3. View sync status including pulled/pushed/applied counts + +The plugin matches Codex series to AniList entries using external IDs stored in the `series_external_ids` table with the `api:anilist` source. + +## Sync Behavior + +### Sync Modes + +You can configure which direction data flows in **Settings** > **Integrations** > **Settings**: + +| Mode | Description | +|------|-------------| +| **Pull & Push** (default) | Import progress from AniList, then export Codex progress to AniList | +| **Pull Only** | Import progress from AniList without writing anything back | +| **Push Only** | Export Codex progress to AniList without importing | + +### How Sync Works + +When sync runs in **Pull & Push** mode, it executes two phases in order: + +1. **Pull** — Fetches your reading list from AniList, matches entries to Codex series via external IDs, and marks the corresponding books as read in Codex. +2. **Push** — Reads your current Codex reading progress and sends it to AniList, overwriting the remote entry. + +### Conflict Resolution + +Codex uses a **highest progress wins** strategy. There is no manual conflict resolution — instead, progress can only move forward: + +| Scenario | Result | +|----------|--------| +| AniList ahead (e.g., 5 vols read) and Codex behind (3 vols read) | Pull marks books 4–5 as read in Codex. Push sends 5 to AniList. Both agree. | +| Codex ahead (5 vols read) and AniList behind (3 vols read) | Pull tries to mark first 3 as read — already completed, skipped. Push sends 5 to AniList. **Codex wins.** | +| Both changed differently | Pull applies remote progress first (additive only), then Push sends local state to AniList. Effectively **Codex wins** because push runs last. | + +Key behaviors: + +- **Pull is additive only** — it marks unread books as read but never un-reads a book. If you lower your chapter count on AniList, that change is ignored. +- **Push overwrites the remote** — after pulling, Codex sends its current state to AniList, which may overwrite changes made directly on AniList. +- **Progress is monotonic** — once a book is marked as read, sync will not undo it. Progress only moves forward. + +### Completed Status + +The plugin is conservative about marking series as "Completed" on AniList: + +- A series is pushed as **Completed** only when all local books are read **and** the series metadata includes a `total_book_count` that matches. +- Otherwise, the series is pushed as **Reading** — even if all local books are read — because Codex can't be sure the library contains the full series. + +### Rating & Notes Sync + +When **Sync Ratings & Notes** is enabled in plugin settings: + +- **Push**: Codex ratings (1-100 scale) and notes are sent to AniList, converted to the user's chosen AniList score format (auto-detected from their profile). +- **Pull**: AniList scores and notes are imported into Codex, but only when Codex has **no existing rating** for that series. Existing Codex ratings are never overwritten (**Codex wins**). +- Notes without a score are skipped on pull (Codex requires a rating to store notes). + +### Auto-Pause & Auto-Drop + +You can configure automatic status changes for series you haven't read in a while: + +| Configuration | Behavior | +|--------------|----------| +| Pause=5, Drop=0 | Not read in 5 days -> Paused | +| Pause=5, Drop=7 | Not read in 5 days -> Paused; not read in 7 days -> Dropped | +| Pause=5, Drop=3 | Not read in 3 days -> Dropped (drop fires first since threshold is shorter) | +| Pause=0, Drop=4 | Not read in 4 days -> Dropped (no pause step) | +| Pause=0, Drop=0 | Disabled (default) | + +Key behaviors: + +- **Only affects in-progress series** — completed series are never auto-paused or auto-dropped. +- **Based on last reading activity** — the timer resets every time you read any book in the series. +- **Drop takes priority** — if both pause and drop thresholds are met, the series is dropped (not paused). + +## Development + +```bash +# Install dependencies +npm install + +# Build the plugin +npm run build + +# Type check +npm run typecheck + +# Run tests +npm test + +# Lint +npm run lint +``` + +## Project Structure + +``` +plugins/sync-anilist/ +├── src/ +│ ├── index.ts # Plugin entry point & staleness logic +│ ├── index.test.ts # Staleness logic tests +│ ├── manifest.ts # Plugin manifest +│ ├── anilist.ts # AniList API client & utility functions +│ └── anilist.test.ts # API client & utility tests +├── dist/ +│ └── index.js # Built bundle (excluded from git) +├── package.json +├── tsconfig.json +└── README.md +``` + +## License + +MIT diff --git a/plugins/sync-anilist/package-lock.json b/plugins/sync-anilist/package-lock.json new file mode 100644 index 00000000..cbaaf7c1 --- /dev/null +++ b/plugins/sync-anilist/package-lock.json @@ -0,0 +1,2277 @@ +{ + "name": "@ashdev/codex-plugin-sync-anilist", + "version": "1.9.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ashdev/codex-plugin-sync-anilist", + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "@ashdev/codex-plugin-sdk": "file:../sdk-typescript" + }, + "bin": { + "codex-plugin-sync-anilist": "dist/index.js" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.13", + "@types/node": "^22.0.0", + "esbuild": "^0.24.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "../sdk-typescript": { + "name": "@ashdev/codex-plugin-sdk", + "version": "1.9.3", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@ashdev/codex-plugin-sdk": { + "resolved": "../sdk-typescript", + "link": true + }, + "node_modules/@biomejs/biome": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.14.tgz", + "integrity": "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.14", + "@biomejs/cli-darwin-x64": "2.3.14", + "@biomejs/cli-linux-arm64": "2.3.14", + "@biomejs/cli-linux-arm64-musl": "2.3.14", + "@biomejs/cli-linux-x64": "2.3.14", + "@biomejs/cli-linux-x64-musl": "2.3.14", + "@biomejs/cli-win32-arm64": "2.3.14", + "@biomejs/cli-win32-x64": "2.3.14" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.14.tgz", + "integrity": "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.14.tgz", + "integrity": "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.14.tgz", + "integrity": "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.14.tgz", + "integrity": "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.14.tgz", + "integrity": "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.14.tgz", + "integrity": "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.14.tgz", + "integrity": "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.14.tgz", + "integrity": "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/plugins/sync-anilist/package.json b/plugins/sync-anilist/package.json new file mode 100644 index 00000000..dcfdaae5 --- /dev/null +++ b/plugins/sync-anilist/package.json @@ -0,0 +1,52 @@ +{ + "name": "@ashdev/codex-plugin-sync-anilist", + "version": "1.9.3", + "description": "AniList reading progress sync plugin for Codex", + "main": "dist/index.js", + "bin": "dist/index.js", + "type": "module", + "files": [ + "dist", + "README.md" + ], + "repository": { + "type": "git", + "url": "https://github.com/AshDevFr/codex.git", + "directory": "plugins/sync-anilist" + }, + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'", + "dev": "npm run build -- --watch", + "clean": "rm -rf dist", + "start": "node dist/index.js", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests", + "test:watch": "vitest", + "prepublishOnly": "npm run lint && npm run build" + }, + "keywords": [ + "codex", + "plugin", + "anilist", + "sync", + "manga", + "reading-progress" + ], + "author": "Codex", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + }, + "dependencies": { + "@ashdev/codex-plugin-sdk": "file:../sdk-typescript" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.13", + "@types/node": "^22.0.0", + "esbuild": "^0.24.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/plugins/sync-anilist/src/anilist.test.ts b/plugins/sync-anilist/src/anilist.test.ts new file mode 100644 index 00000000..87f36714 --- /dev/null +++ b/plugins/sync-anilist/src/anilist.test.ts @@ -0,0 +1,428 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + AniListClient, + anilistStatusToSync, + convertScoreFromAnilist, + convertScoreToAnilist, + fuzzyDateToIso, + isoToFuzzyDate, + syncStatusToAnilist, +} from "./anilist.js"; + +// ============================================================================= +// Status Mapping Tests +// ============================================================================= + +describe("anilistStatusToSync", () => { + it("maps CURRENT to reading", () => { + expect(anilistStatusToSync("CURRENT")).toBe("reading"); + }); + + it("maps REPEATING to reading", () => { + expect(anilistStatusToSync("REPEATING")).toBe("reading"); + }); + + it("maps COMPLETED to completed", () => { + expect(anilistStatusToSync("COMPLETED")).toBe("completed"); + }); + + it("maps PAUSED to on_hold", () => { + expect(anilistStatusToSync("PAUSED")).toBe("on_hold"); + }); + + it("maps DROPPED to dropped", () => { + expect(anilistStatusToSync("DROPPED")).toBe("dropped"); + }); + + it("maps PLANNING to plan_to_read", () => { + expect(anilistStatusToSync("PLANNING")).toBe("plan_to_read"); + }); + + it("maps unknown status to reading", () => { + expect(anilistStatusToSync("UNKNOWN")).toBe("reading"); + }); +}); + +describe("syncStatusToAnilist", () => { + it("maps reading to CURRENT", () => { + expect(syncStatusToAnilist("reading")).toBe("CURRENT"); + }); + + it("maps completed to COMPLETED", () => { + expect(syncStatusToAnilist("completed")).toBe("COMPLETED"); + }); + + it("maps on_hold to PAUSED", () => { + expect(syncStatusToAnilist("on_hold")).toBe("PAUSED"); + }); + + it("maps dropped to DROPPED", () => { + expect(syncStatusToAnilist("dropped")).toBe("DROPPED"); + }); + + it("maps plan_to_read to PLANNING", () => { + expect(syncStatusToAnilist("plan_to_read")).toBe("PLANNING"); + }); + + it("maps unknown status to CURRENT", () => { + expect(syncStatusToAnilist("unknown")).toBe("CURRENT"); + }); +}); + +// ============================================================================= +// Date Conversion Tests +// ============================================================================= + +describe("fuzzyDateToIso", () => { + it("converts full date", () => { + expect(fuzzyDateToIso({ year: 2026, month: 2, day: 6 })).toBe("2026-02-06T00:00:00Z"); + }); + + it("converts year and month only", () => { + expect(fuzzyDateToIso({ year: 2026, month: 3 })).toBe("2026-03-01T00:00:00Z"); + }); + + it("converts year only", () => { + expect(fuzzyDateToIso({ year: 2025 })).toBe("2025-01-01T00:00:00Z"); + }); + + it("returns undefined for null date", () => { + expect(fuzzyDateToIso(null)).toBeUndefined(); + }); + + it("returns undefined for undefined date", () => { + expect(fuzzyDateToIso(undefined)).toBeUndefined(); + }); + + it("returns undefined when year is null", () => { + expect(fuzzyDateToIso({ year: null })).toBeUndefined(); + }); + + it("pads month and day", () => { + expect(fuzzyDateToIso({ year: 2026, month: 1, day: 5 })).toBe("2026-01-05T00:00:00Z"); + }); +}); + +describe("isoToFuzzyDate", () => { + it("converts ISO date string", () => { + const result = isoToFuzzyDate("2026-02-06T00:00:00Z"); + expect(result).toEqual({ year: 2026, month: 2, day: 6 }); + }); + + it("converts ISO datetime", () => { + const result = isoToFuzzyDate("2025-12-25T14:30:00Z"); + expect(result).toEqual({ year: 2025, month: 12, day: 25 }); + }); + + it("returns undefined for undefined input", () => { + expect(isoToFuzzyDate(undefined)).toBeUndefined(); + }); + + it("returns undefined for empty string", () => { + expect(isoToFuzzyDate("")).toBeUndefined(); + }); + + it("returns undefined for invalid date", () => { + expect(isoToFuzzyDate("not-a-date")).toBeUndefined(); + }); +}); + +// ============================================================================= +// Roundtrip Tests +// ============================================================================= + +describe("status roundtrip", () => { + const statuses = [ + { anilist: "CURRENT", sync: "reading" }, + { anilist: "COMPLETED", sync: "completed" }, + { anilist: "PAUSED", sync: "on_hold" }, + { anilist: "DROPPED", sync: "dropped" }, + { anilist: "PLANNING", sync: "plan_to_read" }, + ] as const; + + for (const { anilist, sync } of statuses) { + it(`roundtrips ${anilist} -> ${sync} -> ${anilist}`, () => { + const codexStatus = anilistStatusToSync(anilist); + expect(codexStatus).toBe(sync); + const backToAnilist = syncStatusToAnilist(codexStatus); + expect(backToAnilist).toBe(anilist); + }); + } +}); + +describe("date roundtrip", () => { + it("roundtrips a full date", () => { + const original = { year: 2026, month: 6, day: 15 }; + const iso = fuzzyDateToIso(original); + const result = isoToFuzzyDate(iso); + expect(result).toEqual(original); + }); +}); + +// ============================================================================= +// Score Conversion Tests (1-100 Codex scale <-> AniList formats) +// ============================================================================= + +describe("convertScoreToAnilist (1-100 input)", () => { + it("POINT_100: pass-through", () => { + expect(convertScoreToAnilist(85, "POINT_100")).toBe(85); + expect(convertScoreToAnilist(100, "POINT_100")).toBe(100); + expect(convertScoreToAnilist(1, "POINT_100")).toBe(1); + }); + + it("POINT_10_DECIMAL: divides by 10", () => { + expect(convertScoreToAnilist(85, "POINT_10_DECIMAL")).toBe(8.5); + expect(convertScoreToAnilist(100, "POINT_10_DECIMAL")).toBe(10); + expect(convertScoreToAnilist(10, "POINT_10_DECIMAL")).toBe(1); + }); + + it("POINT_10: rounds to nearest integer after dividing", () => { + expect(convertScoreToAnilist(85, "POINT_10")).toBe(9); + expect(convertScoreToAnilist(84, "POINT_10")).toBe(8); + expect(convertScoreToAnilist(100, "POINT_10")).toBe(10); + expect(convertScoreToAnilist(10, "POINT_10")).toBe(1); + }); + + it("POINT_5: maps to 1-5 scale", () => { + expect(convertScoreToAnilist(100, "POINT_5")).toBe(5); + expect(convertScoreToAnilist(80, "POINT_5")).toBe(4); + expect(convertScoreToAnilist(50, "POINT_5")).toBe(3); + expect(convertScoreToAnilist(20, "POINT_5")).toBe(1); + }); + + it("POINT_3: maps to 1/2/3 based on thresholds", () => { + expect(convertScoreToAnilist(90, "POINT_3")).toBe(3); + expect(convertScoreToAnilist(70, "POINT_3")).toBe(3); + expect(convertScoreToAnilist(69, "POINT_3")).toBe(2); + expect(convertScoreToAnilist(40, "POINT_3")).toBe(2); + expect(convertScoreToAnilist(39, "POINT_3")).toBe(1); + expect(convertScoreToAnilist(1, "POINT_3")).toBe(1); + }); + + it("unknown format: defaults to POINT_10 behavior", () => { + expect(convertScoreToAnilist(80, "UNKNOWN")).toBe(8); + }); +}); + +describe("convertScoreFromAnilist (to 1-100 output)", () => { + it("POINT_100: pass-through", () => { + expect(convertScoreFromAnilist(85, "POINT_100")).toBe(85); + expect(convertScoreFromAnilist(100, "POINT_100")).toBe(100); + expect(convertScoreFromAnilist(1, "POINT_100")).toBe(1); + }); + + it("POINT_10_DECIMAL: multiplies by 10", () => { + expect(convertScoreFromAnilist(8.5, "POINT_10_DECIMAL")).toBe(85); + expect(convertScoreFromAnilist(10, "POINT_10_DECIMAL")).toBe(100); + expect(convertScoreFromAnilist(1, "POINT_10_DECIMAL")).toBe(10); + }); + + it("POINT_10: multiplies by 10", () => { + expect(convertScoreFromAnilist(8, "POINT_10")).toBe(80); + expect(convertScoreFromAnilist(10, "POINT_10")).toBe(100); + expect(convertScoreFromAnilist(1, "POINT_10")).toBe(10); + }); + + it("POINT_5: multiplies by 20", () => { + expect(convertScoreFromAnilist(5, "POINT_5")).toBe(100); + expect(convertScoreFromAnilist(4, "POINT_5")).toBe(80); + expect(convertScoreFromAnilist(1, "POINT_5")).toBe(20); + }); + + it("POINT_3: multiplies by ~33.3", () => { + expect(convertScoreFromAnilist(3, "POINT_3")).toBe(100); + expect(convertScoreFromAnilist(2, "POINT_3")).toBe(67); + expect(convertScoreFromAnilist(1, "POINT_3")).toBe(33); + }); + + it("unknown format: defaults to POINT_10 behavior", () => { + expect(convertScoreFromAnilist(7, "UNKNOWN")).toBe(70); + }); +}); + +describe("score roundtrip", () => { + it("POINT_100 roundtrips exactly", () => { + const codex = 85; + const anilist = convertScoreToAnilist(codex, "POINT_100"); + expect(convertScoreFromAnilist(anilist, "POINT_100")).toBe(codex); + }); + + it("POINT_10_DECIMAL roundtrips exactly", () => { + const codex = 85; + const anilist = convertScoreToAnilist(codex, "POINT_10_DECIMAL"); + expect(convertScoreFromAnilist(anilist, "POINT_10_DECIMAL")).toBe(codex); + }); + + it("POINT_10 roundtrips within ±5", () => { + // 85 -> 9 -> 90 (lossy due to rounding) + const codex = 80; + const anilist = convertScoreToAnilist(codex, "POINT_10"); + expect(convertScoreFromAnilist(anilist, "POINT_10")).toBe(80); + }); + + it("POINT_5 roundtrips within ±10", () => { + const codex = 80; + const anilist = convertScoreToAnilist(codex, "POINT_5"); + expect(convertScoreFromAnilist(anilist, "POINT_5")).toBe(80); + }); +}); + +// ============================================================================= +// AniListClient Fetch Behavior Tests +// ============================================================================= + +describe("AniListClient fetch behavior", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("passes AbortSignal.timeout to fetch", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ data: { Viewer: { id: 1, name: "test" } } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListClient("test-token"); + await client.getViewer(); + + expect(fetchSpy).toHaveBeenCalledOnce(); + const callArgs = fetchSpy.mock.calls[0]; + const init = callArgs[1] as RequestInit; + expect(init.signal).toBeDefined(); + }); + + it("wraps timeout errors with descriptive message", async () => { + const timeoutError = new DOMException( + "The operation was aborted due to timeout", + "TimeoutError", + ); + vi.spyOn(globalThis, "fetch").mockRejectedValue(timeoutError); + + const client = new AniListClient("test-token"); + await expect(client.getViewer()).rejects.toThrow( + "AniList API request timed out after 30 seconds", + ); + }); + + it("re-throws non-timeout fetch errors as-is", async () => { + vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network failure")); + + const client = new AniListClient("test-token"); + await expect(client.getViewer()).rejects.toThrow("Network failure"); + }); + + it("retries once on 429 then succeeds", async () => { + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce( + new Response("", { + status: 429, + headers: { "Retry-After": "0" }, + }), + ) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + data: { + Viewer: { + id: 1, + name: "test", + avatar: {}, + siteUrl: "", + options: { displayAdultContent: false }, + mediaListOptions: { scoreFormat: "POINT_10" }, + }, + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ), + ); + + const client = new AniListClient("test-token"); + const viewer = await client.getViewer(); + + expect(viewer.id).toBe(1); + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); + + it("throws RateLimitError after retry exhausted on 429", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("", { + status: 429, + headers: { "Retry-After": "0" }, + }), + ); + + const client = new AniListClient("test-token"); + await expect(client.getViewer()).rejects.toThrow("AniList rate limit exceeded"); + }); +}); + +// ============================================================================= +// searchManga Tests +// ============================================================================= + +describe("AniListClient.searchManga", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("returns search result when found", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response( + JSON.stringify({ + data: { + Media: { id: 42, title: { romaji: "Berserk", english: "Berserk" } }, + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ), + ); + + const client = new AniListClient("test-token"); + const result = await client.searchManga("Berserk"); + + expect(result).not.toBeNull(); + expect(result?.id).toBe(42); + expect(result?.title.english).toBe("Berserk"); + }); + + it("returns null when Media is null", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ data: { Media: null } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListClient("test-token"); + const result = await client.searchManga("Nonexistent Manga"); + + expect(result).toBeNull(); + }); + + it("returns null on API error (swallows exceptions)", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( + new Response(JSON.stringify({ errors: [{ message: "Not found" }] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new AniListClient("test-token"); + const result = await client.searchManga("Error Manga"); + + expect(result).toBeNull(); + }); + + it("returns null on network error", async () => { + vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("Network failure")); + + const client = new AniListClient("test-token"); + const result = await client.searchManga("Network Manga"); + + expect(result).toBeNull(); + }); +}); diff --git a/plugins/sync-anilist/src/anilist.ts b/plugins/sync-anilist/src/anilist.ts new file mode 100644 index 00000000..ae951cc8 --- /dev/null +++ b/plugins/sync-anilist/src/anilist.ts @@ -0,0 +1,445 @@ +/** + * AniList GraphQL API client + * + * Provides typed access to AniList's GraphQL API for reading progress sync. + * See: https://anilist.gitbook.io/anilist-apiv2-docs/ + */ + +import { ApiError, AuthError, RateLimitError } from "@ashdev/codex-plugin-sdk"; + +const ANILIST_API_URL = "https://graphql.anilist.co"; + +// ============================================================================= +// GraphQL Queries +// ============================================================================= + +const VIEWER_QUERY = ` + query { + Viewer { + id + name + avatar { + large + medium + } + siteUrl + options { + displayAdultContent + } + mediaListOptions { + scoreFormat + } + } + } +`; + +const MANGA_LIST_QUERY = ` + query ($userId: Int!, $page: Int, $perPage: Int) { + Page(page: $page, perPage: $perPage) { + pageInfo { + total + currentPage + lastPage + hasNextPage + } + mediaList(userId: $userId, type: MANGA, sort: UPDATED_TIME_DESC) { + id + mediaId + status + score + progress + progressVolumes + startedAt { + year + month + day + } + completedAt { + year + month + day + } + notes + updatedAt + media { + id + title { + romaji + english + native + } + siteUrl + } + } + } + } +`; + +/** Search for a manga by title to find its AniList ID */ +const SEARCH_MANGA_QUERY = ` + query ($search: String!) { + Media(search: $search, type: MANGA) { + id + title { + romaji + english + } + } + } +`; + +const UPDATE_ENTRY_MUTATION = ` + mutation ( + $mediaId: Int!, + $status: MediaListStatus, + $score: Float, + $progress: Int, + $progressVolumes: Int, + $startedAt: FuzzyDateInput, + $completedAt: FuzzyDateInput, + $notes: String + ) { + SaveMediaListEntry( + mediaId: $mediaId, + status: $status, + score: $score, + progress: $progress, + progressVolumes: $progressVolumes, + startedAt: $startedAt, + completedAt: $completedAt, + notes: $notes + ) { + id + mediaId + status + score + progress + progressVolumes + } + } +`; + +// ============================================================================= +// Types +// ============================================================================= + +export interface AniListViewer { + id: number; + name: string; + avatar: { large?: string; medium?: string }; + siteUrl: string; + options: { displayAdultContent: boolean }; + mediaListOptions: { scoreFormat: string }; +} + +export interface AniListSearchResult { + id: number; + title: { romaji?: string; english?: string }; +} + +export interface AniListFuzzyDate { + year?: number | null; + month?: number | null; + day?: number | null; +} + +export interface AniListMediaListEntry { + id: number; + mediaId: number; + status: string; + score: number; + progress: number; + progressVolumes: number; + startedAt: AniListFuzzyDate; + completedAt: AniListFuzzyDate; + notes: string | null; + updatedAt: number; + media: { + id: number; + title: { romaji?: string; english?: string; native?: string }; + siteUrl: string; + }; +} + +export interface AniListPageInfo { + total: number; + currentPage: number; + lastPage: number; + hasNextPage: boolean; +} + +export interface AniListSaveResult { + id: number; + mediaId: number; + status: string; + score: number; + progress: number; + progressVolumes: number; +} + +// ============================================================================= +// Client +// ============================================================================= + +export class AniListClient { + private accessToken: string; + + constructor(accessToken: string) { + this.accessToken = accessToken; + } + + /** + * Execute a GraphQL query against the AniList API. + * On rate limit (429), waits the requested duration and retries once. + */ + private async query(queryStr: string, variables?: Record): Promise { + return this.executeQuery(queryStr, variables, true); + } + + private async executeQuery( + queryStr: string, + variables: Record | undefined, + allowRetry: boolean, + ): Promise { + let response: Response; + try { + response = await fetch(ANILIST_API_URL, { + method: "POST", + signal: AbortSignal.timeout(30_000), + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${this.accessToken}`, + }, + body: JSON.stringify({ query: queryStr, variables }), + }); + } catch (error) { + if (error instanceof DOMException && error.name === "TimeoutError") { + throw new ApiError("AniList API request timed out after 30 seconds"); + } + throw error; + } + + if (response.status === 401) { + throw new AuthError("AniList access token is invalid or expired"); + } + + if (response.status === 429) { + const retryAfter = response.headers.get("Retry-After"); + const retrySeconds = retryAfter ? Number.parseInt(retryAfter, 10) : 60; + const waitSeconds = Number.isNaN(retrySeconds) ? 60 : retrySeconds; + + if (allowRetry) { + await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); + return this.executeQuery(queryStr, variables, false); + } + + throw new RateLimitError(waitSeconds, "AniList rate limit exceeded"); + } + + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new ApiError( + `AniList API error: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`, + ); + } + + const json = (await response.json()) as { data?: T; errors?: Array<{ message: string }> }; + + if (json.errors?.length) { + const message = json.errors.map((e) => e.message).join("; "); + throw new ApiError(`AniList GraphQL error: ${message}`); + } + + if (!json.data) { + throw new ApiError("AniList returned empty data"); + } + + return json.data; + } + + /** + * Get the authenticated user's info + */ + async getViewer(): Promise { + const data = await this.query<{ Viewer: AniListViewer }>(VIEWER_QUERY); + return data.Viewer; + } + + /** + * Get the user's manga list (paginated) + */ + async getMangaList( + userId: number, + page = 1, + perPage = 50, + ): Promise<{ pageInfo: AniListPageInfo; entries: AniListMediaListEntry[] }> { + const variables: Record = { userId, page, perPage }; + + const data = await this.query<{ + Page: { + pageInfo: AniListPageInfo; + mediaList: AniListMediaListEntry[]; + }; + }>(MANGA_LIST_QUERY, variables); + + return { + pageInfo: data.Page.pageInfo, + entries: data.Page.mediaList, + }; + } + + /** + * Update or create a manga list entry + */ + async saveEntry(variables: { + mediaId: number; + status?: string; + score?: number; + progress?: number; + progressVolumes?: number; + startedAt?: AniListFuzzyDate; + completedAt?: AniListFuzzyDate; + notes?: string; + }): Promise { + const data = await this.query<{ SaveMediaListEntry: AniListSaveResult }>( + UPDATE_ENTRY_MUTATION, + variables, + ); + return data.SaveMediaListEntry; + } + + /** + * Search for a manga by title and return its AniList ID. + * Returns null if no result found or an error occurs. + */ + async searchManga(title: string): Promise { + try { + const data = await this.query<{ Media: AniListSearchResult | null }>(SEARCH_MANGA_QUERY, { + search: title, + }); + return data.Media; + } catch { + return null; + } + } +} + +// ============================================================================= +// Status Mapping +// ============================================================================= + +/** + * Map AniList MediaListStatus to Codex SyncReadingStatus + */ +export function anilistStatusToSync( + status: string, +): "reading" | "completed" | "on_hold" | "dropped" | "plan_to_read" { + switch (status) { + case "CURRENT": + case "REPEATING": + return "reading"; + case "COMPLETED": + return "completed"; + case "PAUSED": + return "on_hold"; + case "DROPPED": + return "dropped"; + case "PLANNING": + return "plan_to_read"; + default: + return "reading"; + } +} + +/** + * Map Codex SyncReadingStatus to AniList MediaListStatus + */ +export function syncStatusToAnilist( + status: string, +): "CURRENT" | "COMPLETED" | "PAUSED" | "DROPPED" | "PLANNING" { + switch (status) { + case "reading": + return "CURRENT"; + case "completed": + return "COMPLETED"; + case "on_hold": + return "PAUSED"; + case "dropped": + return "DROPPED"; + case "plan_to_read": + return "PLANNING"; + default: + return "CURRENT"; + } +} + +/** + * Convert AniList FuzzyDate to ISO 8601 string + */ +export function fuzzyDateToIso(date: AniListFuzzyDate | null | undefined): string | undefined { + if (!date?.year) return undefined; + const month = date.month ? String(date.month).padStart(2, "0") : "01"; + const day = date.day ? String(date.day).padStart(2, "0") : "01"; + return `${date.year}-${month}-${day}T00:00:00Z`; +} + +/** + * Convert ISO 8601 string to AniList FuzzyDate + */ +export function isoToFuzzyDate(iso: string | undefined): AniListFuzzyDate | undefined { + if (!iso) return undefined; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return undefined; + return { + year: d.getUTCFullYear(), + month: d.getUTCMonth() + 1, + day: d.getUTCDate(), + }; +} + +// ============================================================================= +// Score Conversion +// ============================================================================= + +/** + * Convert a score from Codex's 1-100 scale to AniList's format + */ +export function convertScoreToAnilist(score: number, format: string): number { + switch (format) { + case "POINT_100": + return Math.round(score); + case "POINT_10_DECIMAL": + return score / 10; + case "POINT_10": + return Math.round(score / 10); + case "POINT_5": + return Math.round(score / 20); + case "POINT_3": + if (score >= 70) return 3; + if (score >= 40) return 2; + return 1; + default: + return Math.round(score / 10); + } +} + +/** + * Convert a score from AniList's format to Codex's 1-100 scale + */ +export function convertScoreFromAnilist(score: number, format: string): number { + switch (format) { + case "POINT_100": + return score; + case "POINT_10_DECIMAL": + return score * 10; + case "POINT_10": + return score * 10; + case "POINT_5": + return score * 20; + case "POINT_3": + return Math.round(score * 33.3); + default: + return score * 10; + } +} diff --git a/plugins/sync-anilist/src/index.test.ts b/plugins/sync-anilist/src/index.test.ts new file mode 100644 index 00000000..0faa7f62 --- /dev/null +++ b/plugins/sync-anilist/src/index.test.ts @@ -0,0 +1,267 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AniListClient } from "./anilist.js"; +import { applyStaleness, provider, setClient, setSearchFallback, setViewerId } from "./index.js"; + +// ============================================================================= +// applyStaleness Tests +// ============================================================================= + +describe("applyStaleness", () => { + // Helper: returns a timestamp N days ago from a fixed reference point + const now = new Date("2026-02-08T12:00:00Z").getTime(); + const daysAgo = (days: number) => new Date(now - days * 24 * 60 * 60 * 1000).toISOString(); + + describe("passthrough cases", () => { + it("returns status unchanged when not reading", () => { + expect(applyStaleness("completed", daysAgo(100), 30, 60, now)).toBe("completed"); + expect(applyStaleness("on_hold", daysAgo(100), 30, 60, now)).toBe("on_hold"); + expect(applyStaleness("dropped", daysAgo(100), 30, 60, now)).toBe("dropped"); + expect(applyStaleness("plan_to_read", daysAgo(100), 30, 60, now)).toBe("plan_to_read"); + }); + + it("returns reading when both thresholds are 0 (disabled)", () => { + expect(applyStaleness("reading", daysAgo(365), 0, 0, now)).toBe("reading"); + }); + + it("returns reading when latestUpdatedAt is undefined", () => { + expect(applyStaleness("reading", undefined, 30, 60, now)).toBe("reading"); + }); + + it("returns reading when latestUpdatedAt is invalid", () => { + expect(applyStaleness("reading", "not-a-date", 30, 60, now)).toBe("reading"); + }); + + it("returns reading when activity is recent", () => { + expect(applyStaleness("reading", daysAgo(5), 30, 60, now)).toBe("reading"); + }); + }); + + describe("pause only (drop disabled)", () => { + it("pauses after threshold", () => { + expect(applyStaleness("reading", daysAgo(31), 30, 0, now)).toBe("on_hold"); + }); + + it("pauses at exact threshold", () => { + expect(applyStaleness("reading", daysAgo(30), 30, 0, now)).toBe("on_hold"); + }); + + it("does not pause below threshold", () => { + expect(applyStaleness("reading", daysAgo(29), 30, 0, now)).toBe("reading"); + }); + }); + + describe("drop only (pause disabled)", () => { + it("drops after threshold", () => { + expect(applyStaleness("reading", daysAgo(61), 0, 60, now)).toBe("dropped"); + }); + + it("drops at exact threshold", () => { + expect(applyStaleness("reading", daysAgo(60), 0, 60, now)).toBe("dropped"); + }); + + it("does not drop below threshold", () => { + expect(applyStaleness("reading", daysAgo(59), 0, 60, now)).toBe("reading"); + }); + }); + + describe("both pause and drop enabled", () => { + it("pauses when inactive past pause but not drop threshold", () => { + // pause=30, drop=60, inactive=45 → pause + expect(applyStaleness("reading", daysAgo(45), 30, 60, now)).toBe("on_hold"); + }); + + it("drops when inactive past both thresholds (drop takes priority)", () => { + // pause=30, drop=60, inactive=90 → drop (stronger action) + expect(applyStaleness("reading", daysAgo(90), 30, 60, now)).toBe("dropped"); + }); + + it("drops at exact drop threshold even when pause threshold is also met", () => { + expect(applyStaleness("reading", daysAgo(60), 30, 60, now)).toBe("dropped"); + }); + + it("does nothing when active within both thresholds", () => { + expect(applyStaleness("reading", daysAgo(10), 30, 60, now)).toBe("reading"); + }); + }); + + describe("edge cases", () => { + it("handles future latestUpdatedAt (0 days inactive)", () => { + const future = new Date(now + 24 * 60 * 60 * 1000).toISOString(); + expect(applyStaleness("reading", future, 30, 60, now)).toBe("reading"); + }); + + it("handles very old latestUpdatedAt", () => { + expect(applyStaleness("reading", "2020-01-01T00:00:00Z", 30, 60, now)).toBe("dropped"); + }); + + it("uses Date.now() when now parameter is omitted", () => { + // Activity 1000 days ago with threshold of 1 day → should pause + const veryOld = new Date(Date.now() - 1000 * 24 * 60 * 60 * 1000).toISOString(); + expect(applyStaleness("reading", veryOld, 1, 0)).toBe("on_hold"); + }); + }); +}); + +// ============================================================================= +// pushProgress — searchFallback toggle Tests +// ============================================================================= + +describe("pushProgress searchFallback", () => { + function makeMockClient(overrides?: { + searchManga?: AniListClient["searchManga"]; + saveEntry?: AniListClient["saveEntry"]; + getMangaList?: AniListClient["getMangaList"]; + }) { + return { + getViewer: vi.fn(), + getMangaList: + overrides?.getMangaList ?? + vi.fn().mockResolvedValue({ + pageInfo: { total: 0, currentPage: 1, lastPage: 1, hasNextPage: false }, + entries: [], + }), + saveEntry: + overrides?.saveEntry ?? + vi.fn().mockResolvedValue({ + id: 1, + mediaId: 42, + status: "CURRENT", + score: 0, + progress: 0, + progressVolumes: 1, + }), + searchManga: overrides?.searchManga ?? vi.fn().mockResolvedValue(null), + } as unknown as AniListClient; + } + + afterEach(() => { + setClient(null); + setViewerId(null); + setSearchFallback(false); // restore default + }); + + it("resolves entry via searchManga when searchFallback=true and externalId is empty", async () => { + setSearchFallback(true); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 42, title: { english: "One Piece" } }), + }); + setClient(mockClient); + setViewerId(1); + + const result = await provider.pushProgress({ + entries: [ + { + externalId: "", + title: "One Piece", + status: "reading", + progress: { volumes: 5 }, + }, + ], + }); + + expect(result.success).toHaveLength(1); + expect(result.failed).toHaveLength(0); + expect(result.success[0].externalId).toBe("42"); + expect(result.success[0].status).toBe("created"); + expect(mockClient.searchManga).toHaveBeenCalledWith("One Piece"); + }); + + it("fails entry when searchFallback=false and externalId is empty", async () => { + setSearchFallback(false); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 42, title: { english: "One Piece" } }), + }); + setClient(mockClient); + setViewerId(1); + + const result = await provider.pushProgress({ + entries: [ + { + externalId: "", + title: "One Piece", + status: "reading", + progress: { volumes: 5 }, + }, + ], + }); + + expect(result.success).toHaveLength(0); + expect(result.failed).toHaveLength(1); + expect(result.failed[0].status).toBe("failed"); + expect(result.failed[0].error).toContain("Invalid media ID"); + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); + + it("fails entry when searchFallback=true but search returns no result", async () => { + setSearchFallback(true); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue(null), + }); + setClient(mockClient); + setViewerId(1); + + const result = await provider.pushProgress({ + entries: [ + { + externalId: "", + title: "Obscure Manga", + status: "reading", + progress: { volumes: 1 }, + }, + ], + }); + + expect(result.success).toHaveLength(0); + expect(result.failed).toHaveLength(1); + expect(result.failed[0].error).toContain("No AniList match found"); + expect(mockClient.searchManga).toHaveBeenCalledWith("Obscure Manga"); + }); + + it("does not call searchManga when externalId is a valid number", async () => { + setSearchFallback(true); + const mockClient = makeMockClient(); + setClient(mockClient); + setViewerId(1); + + const result = await provider.pushProgress({ + entries: [ + { + externalId: "42", + status: "reading", + progress: { volumes: 3 }, + }, + ], + }); + + expect(result.success).toHaveLength(1); + expect(result.success[0].externalId).toBe("42"); + expect(mockClient.searchManga).not.toHaveBeenCalled(); + }); + + it("reports 'updated' when mediaId already exists in user list", async () => { + setSearchFallback(true); + const mockClient = makeMockClient({ + searchManga: vi.fn().mockResolvedValue({ id: 100, title: { english: "Known" } }), + getMangaList: vi.fn().mockResolvedValue({ + pageInfo: { total: 1, currentPage: 1, lastPage: 1, hasNextPage: false }, + entries: [{ mediaId: 100 }], + }), + }); + setClient(mockClient); + setViewerId(1); + + const result = await provider.pushProgress({ + entries: [ + { + externalId: "", + title: "Known", + status: "reading", + progress: { volumes: 2 }, + }, + ], + }); + + expect(result.success).toHaveLength(1); + expect(result.success[0].status).toBe("updated"); + }); +}); diff --git a/plugins/sync-anilist/src/index.ts b/plugins/sync-anilist/src/index.ts new file mode 100644 index 00000000..f52ec80b --- /dev/null +++ b/plugins/sync-anilist/src/index.ts @@ -0,0 +1,358 @@ +/** + * AniList Sync Plugin for Codex + * + * Syncs manga reading progress between Codex and AniList. + * Communicates via JSON-RPC over stdio using the Codex plugin SDK. + * + * Capabilities: + * - Push reading progress from Codex to AniList + * - Pull reading progress from AniList to Codex + * - Get user info from AniList + * - Status reporting for sync state + */ + +import { + createLogger, + createSyncPlugin, + type ExternalUserInfo, + type InitializeParams, + type SyncEntry, + type SyncEntryResult, + type SyncProvider, + type SyncPullRequest, + type SyncPullResponse, + type SyncPushRequest, + type SyncPushResponse, + type SyncStatusResponse, +} from "@ashdev/codex-plugin-sdk"; +import { + AniListClient, + type AniListFuzzyDate, + anilistStatusToSync, + convertScoreFromAnilist, + convertScoreToAnilist, + fuzzyDateToIso, + isoToFuzzyDate, + syncStatusToAnilist, +} from "./anilist.js"; +import { manifest } from "./manifest.js"; + +const logger = createLogger({ name: "sync-anilist", level: "debug" }); + +// Plugin state (set during initialization) +let client: AniListClient | null = null; +let viewerId: number | null = null; +let scoreFormat = "POINT_10"; + +// Plugin-specific config (from userConfig, set during initialization) +let progressUnit: "volumes" | "chapters" = "volumes"; +let pauseAfterDays = 0; +let dropAfterDays = 0; +let searchFallback = false; + +/** Set the AniList client (exported for testing) */ +export function setClient(c: AniListClient | null): void { + client = c; +} + +/** Set the viewer ID (exported for testing) */ +export function setViewerId(id: number | null): void { + viewerId = id; +} + +/** Set the searchFallback flag (exported for testing) */ +export function setSearchFallback(enabled: boolean): void { + searchFallback = enabled; +} + +// ============================================================================= +// Staleness Logic +// ============================================================================= + +/** + * Apply auto-pause/auto-drop for stale in-progress entries. + * + * Only applies to "reading" entries. Drop takes priority over pause + * when both thresholds are met. A threshold of 0 means disabled. + */ +export function applyStaleness( + status: SyncEntry["status"], + latestUpdatedAt: string | undefined, + pauseDays: number, + dropDays: number, + now?: number, +): SyncEntry["status"] { + if (status !== "reading") return status; + if (pauseDays === 0 && dropDays === 0) return status; + if (!latestUpdatedAt) return status; + + const lastActivity = new Date(latestUpdatedAt).getTime(); + if (Number.isNaN(lastActivity)) return status; + + const currentTime = now ?? Date.now(); + const daysInactive = Math.max(0, (currentTime - lastActivity) / (1000 * 60 * 60 * 24)); + + // Drop takes priority (stronger action) + if (dropDays > 0 && daysInactive >= dropDays) { + return "dropped"; + } + if (pauseDays > 0 && daysInactive >= pauseDays) { + return "on_hold"; + } + + return status; +} + +// ============================================================================= +// Sync Provider Implementation +// ============================================================================= + +/** Exported for testing */ +export const provider: SyncProvider = { + async getUserInfo(): Promise { + if (!client) { + throw new Error("Plugin not initialized - no AniList client"); + } + + const viewer = await client.getViewer(); + viewerId = viewer.id; + scoreFormat = viewer.mediaListOptions.scoreFormat; + + logger.info(`Authenticated as ${viewer.name} (id: ${viewer.id}, scoreFormat: ${scoreFormat})`); + + return { + externalId: String(viewer.id), + username: viewer.name, + avatarUrl: viewer.avatar.large || viewer.avatar.medium, + profileUrl: viewer.siteUrl, + }; + }, + + async pushProgress(params: SyncPushRequest): Promise { + if (!client || viewerId === null) { + throw new Error("Plugin not initialized - call getUserInfo first"); + } + + // Pre-fetch existing media IDs to distinguish "created" vs "updated" + const existingMediaIds = new Set(); + let page = 1; + let hasMore = true; + while (hasMore) { + const result = await client.getMangaList(viewerId, page, 50); + for (const entry of result.entries) { + existingMediaIds.add(entry.mediaId); + } + hasMore = result.pageInfo.hasNextPage; + page++; + } + + const success: SyncEntryResult[] = []; + const failed: SyncEntryResult[] = []; + + for (const entry of params.entries) { + try { + let mediaId = Number.parseInt(entry.externalId, 10); + if (Number.isNaN(mediaId)) { + // Try search fallback if enabled and entry has a title + if (searchFallback && entry.title) { + const result = await client.searchManga(entry.title); + if (result) { + mediaId = result.id; + logger.info(`Search fallback resolved "${entry.title}" → AniList ID ${mediaId}`); + } + } + + if (Number.isNaN(mediaId)) { + failed.push({ + externalId: entry.externalId, + status: "failed", + error: searchFallback + ? `No AniList match found for "${entry.title || entry.externalId}"` + : `Invalid media ID: ${entry.externalId}`, + }); + continue; + } + } + + // Apply staleness logic: auto-pause or auto-drop stale in-progress entries + const effectiveStatus = applyStaleness( + entry.status, + entry.latestUpdatedAt, + pauseAfterDays, + dropAfterDays, + ); + if (effectiveStatus !== entry.status) { + logger.debug( + `Entry ${entry.externalId}: auto-${effectiveStatus === "dropped" ? "dropped" : "paused"} (was ${entry.status})`, + ); + } + + const saveParams: { + mediaId: number; + status?: string; + score?: number; + progress?: number; + progressVolumes?: number; + startedAt?: AniListFuzzyDate; + completedAt?: AniListFuzzyDate; + notes?: string; + } = { + mediaId, + status: syncStatusToAnilist(effectiveStatus), + }; + + // Map progress using the configured progressUnit. + // Server always sends books-read as `volumes`. Based on + // progressUnit, we map to AniList's `progress` (chapters) + // or `progressVolumes` (volumes) field. + const count = entry.progress?.volumes ?? entry.progress?.chapters; + if (count !== undefined) { + if (progressUnit === "chapters") { + saveParams.progress = count; + } else { + saveParams.progressVolumes = count; + } + } + + // Map score (convert from 1-100 scale to AniList format) + if (entry.score !== undefined) { + saveParams.score = convertScoreToAnilist(entry.score, scoreFormat); + } + + // Map dates + if (entry.startedAt) { + saveParams.startedAt = isoToFuzzyDate(entry.startedAt); + } + if (entry.completedAt) { + saveParams.completedAt = isoToFuzzyDate(entry.completedAt); + } + + // Map notes + if (entry.notes !== undefined) { + saveParams.notes = entry.notes; + } + + const resolvedExternalId = String(mediaId); + const existed = existingMediaIds.has(mediaId); + const result = await client.saveEntry(saveParams); + logger.debug(`Pushed entry ${resolvedExternalId}: status=${result.status}`); + + // Track newly created entries for subsequent pushes in the same batch + existingMediaIds.add(mediaId); + + success.push({ + externalId: resolvedExternalId, + status: existed ? "updated" : "created", + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + logger.error(`Failed to push entry ${entry.externalId}: ${message}`); + failed.push({ + externalId: entry.externalId, + status: "failed", + error: message, + }); + } + } + + return { success, failed }; + }, + + async pullProgress(params: SyncPullRequest): Promise { + if (!client || viewerId === null) { + throw new Error("Plugin not initialized - call getUserInfo first"); + } + + // Parse pagination cursor (page number) + const page = params.cursor ? Number.parseInt(params.cursor, 10) : 1; + const perPage = params.limit ? Math.min(params.limit, 50) : 50; + + const result = await client.getMangaList(viewerId, page, perPage); + + const entries: SyncEntry[] = result.entries.map((entry) => ({ + externalId: String(entry.mediaId), + status: anilistStatusToSync(entry.status), + progress: { + chapters: entry.progress || undefined, + volumes: entry.progressVolumes || undefined, + }, + score: entry.score > 0 ? convertScoreFromAnilist(entry.score, scoreFormat) : undefined, + startedAt: fuzzyDateToIso(entry.startedAt), + completedAt: fuzzyDateToIso(entry.completedAt), + notes: entry.notes || undefined, + })); + + logger.info( + `Pulled ${entries.length} entries (page ${result.pageInfo.currentPage}/${result.pageInfo.lastPage})`, + ); + + return { + entries, + nextCursor: result.pageInfo.hasNextPage ? String(result.pageInfo.currentPage + 1) : undefined, + hasMore: result.pageInfo.hasNextPage, + }; + }, + + async status(): Promise { + if (!client || viewerId === null) { + return { + pendingPush: 0, + pendingPull: 0, + conflicts: 0, + }; + } + + // Get total count from AniList + const result = await client.getMangaList(viewerId, 1, 1); + + return { + externalCount: result.pageInfo.total, + pendingPush: 0, + pendingPull: 0, + conflicts: 0, + }; + }, +}; + +// ============================================================================= +// Plugin Initialization +// ============================================================================= + +createSyncPlugin({ + manifest, + provider, + logLevel: "debug", + onInitialize(params: InitializeParams) { + // Get access token from credentials + const accessToken = params.credentials?.access_token; + if (accessToken) { + client = new AniListClient(accessToken); + logger.info("AniList client initialized with access token"); + } else { + logger.warn("No access token provided - sync operations will fail"); + } + + // Read plugin-specific config from userConfig + const uc = params.userConfig; + if (uc) { + const unit = uc.progressUnit; + if (unit === "chapters" || unit === "volumes") { + progressUnit = unit; + } + if (typeof uc.pauseAfterDays === "number" && uc.pauseAfterDays >= 0) { + pauseAfterDays = uc.pauseAfterDays; + } + if (typeof uc.dropAfterDays === "number" && uc.dropAfterDays >= 0) { + dropAfterDays = uc.dropAfterDays; + } + if (typeof uc.searchFallback === "boolean") { + searchFallback = uc.searchFallback; + } + logger.info( + `Plugin config: progressUnit=${progressUnit}, pauseAfterDays=${pauseAfterDays}, dropAfterDays=${dropAfterDays}, searchFallback=${searchFallback}`, + ); + } + }, +}); + +logger.info("AniList sync plugin started"); diff --git a/plugins/sync-anilist/src/manifest.ts b/plugins/sync-anilist/src/manifest.ts new file mode 100644 index 00000000..08b0ab19 --- /dev/null +++ b/plugins/sync-anilist/src/manifest.ts @@ -0,0 +1,81 @@ +import { EXTERNAL_ID_SOURCE_ANILIST, type PluginManifest } from "@ashdev/codex-plugin-sdk"; +import packageJson from "../package.json" with { type: "json" }; + +export const manifest = { + name: "sync-anilist", + displayName: "AniList Sync", + version: packageJson.version, + description: + "Sync manga reading progress between Codex and AniList. Supports push/pull of reading status, chapters read, scores, and dates.", + author: "Codex", + homepage: "https://github.com/AshDevFr/codex", + protocolVersion: "1.0", + capabilities: { + userReadSync: true, + externalIdSource: EXTERNAL_ID_SOURCE_ANILIST, + }, + requiredCredentials: [ + { + key: "access_token", + label: "AniList Access Token", + description: "OAuth access token for AniList API", + type: "password" as const, + required: true, + sensitive: true, + }, + ], + userConfigSchema: { + description: "AniList-specific sync settings", + fields: [ + { + key: "progressUnit", + label: "Progress Unit", + description: + "What each book in Codex represents in AniList. Use 'volumes' for manga volumes, 'chapters' for individual chapters", + type: "string" as const, + required: false, + default: "volumes", + }, + { + key: "pauseAfterDays", + label: "Auto-Pause After Days", + description: + "Automatically set in-progress series to Paused on AniList if no reading activity in this many days. Set to 0 to disable.", + type: "number" as const, + required: false, + default: 0, + }, + { + key: "dropAfterDays", + label: "Auto-Drop After Days", + description: + "Automatically set in-progress series to Dropped on AniList if no reading activity in this many days. Set to 0 to disable. When both pause and drop are set, the shorter threshold fires first.", + type: "number" as const, + required: false, + default: 0, + }, + { + key: "searchFallback", + label: "Search Fallback", + description: + "When a series has no AniList ID, search by title to find a match and sync progress. Disable for strict matching only.", + type: "boolean" as const, + required: false, + default: false, + }, + ], + }, + oauth: { + authorizationUrl: "https://anilist.co/api/v2/oauth/authorize", + tokenUrl: "https://anilist.co/api/v2/oauth/token", + scopes: [], + pkce: false, + }, + userDescription: "Sync manga reading progress between Codex and AniList", + adminSetupInstructions: + "To enable OAuth login, create an AniList API client at https://anilist.co/settings/developer. Set the redirect URL to {your-codex-url}/api/v1/user/plugins/oauth/callback. Enter the Client ID below. Without OAuth configured, users can still connect by pasting a personal access token.", + userSetupInstructions: + "Connect your AniList account via OAuth, or paste a personal access token. To generate a token, visit https://anilist.co/settings/developer, create a client with redirect URL https://anilist.co/api/v2/oauth/pin, then authorize it to receive your token.", +} as const satisfies PluginManifest & { + capabilities: { userReadSync: true }; +}; diff --git a/plugins/sync-anilist/tsconfig.json b/plugins/sync-anilist/tsconfig.json new file mode 100644 index 00000000..ef1ca5f9 --- /dev/null +++ b/plugins/sync-anilist/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/plugins/sync-anilist/vitest.config.ts b/plugins/sync-anilist/vitest.config.ts new file mode 100644 index 00000000..ae847ff6 --- /dev/null +++ b/plugins/sync-anilist/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/src/api/docs.rs b/src/api/docs.rs index 00b24db6..e5a81d75 100644 --- a/src/api/docs.rs +++ b/src/api/docs.rs @@ -412,6 +412,24 @@ The following paths are exempt from rate limiting: v1::handlers::plugin_actions::enqueue_bulk_auto_match_tasks, v1::handlers::plugin_actions::enqueue_library_auto_match_tasks, + // User Plugin endpoints + v1::handlers::user_plugins::list_user_plugins, + v1::handlers::user_plugins::enable_plugin, + v1::handlers::user_plugins::disable_plugin, + v1::handlers::user_plugins::disconnect_plugin, + v1::handlers::user_plugins::get_user_plugin, + v1::handlers::user_plugins::update_user_plugin_config, + v1::handlers::user_plugins::oauth_start, + v1::handlers::user_plugins::oauth_callback, + v1::handlers::user_plugins::set_user_credentials, + v1::handlers::user_plugins::trigger_sync, + v1::handlers::user_plugins::get_sync_status, + + // Recommendation endpoints + v1::handlers::recommendations::get_recommendations, + v1::handlers::recommendations::refresh_recommendations, + v1::handlers::recommendations::dismiss_recommendation, + // Sharing Tags endpoints v1::handlers::sharing_tags::list_sharing_tags, v1::handlers::sharing_tags::get_sharing_tag, @@ -755,6 +773,7 @@ The following paths are exempt from rate limiting: v1::dto::UpdatePluginRequest, v1::dto::EnvVarDto, v1::dto::PluginManifestDto, + v1::dto::OAuthConfigDto, v1::dto::PluginCapabilitiesDto, v1::dto::CredentialFieldDto, v1::dto::PluginTestResult, @@ -764,6 +783,25 @@ The following paths are exempt from rate limiting: v1::dto::PluginFailureDto, v1::dto::PluginFailuresResponse, + // User Plugin DTOs + v1::dto::UserPluginDto, + v1::dto::AvailablePluginDto, + v1::dto::UserPluginCapabilitiesDto, + v1::dto::UserPluginsListResponse, + v1::dto::OAuthStartResponse, + v1::dto::UpdateUserPluginConfigRequest, + v1::dto::SetUserCredentialsRequest, + v1::dto::SyncTriggerResponse, + v1::dto::SyncStatusDto, + v1::dto::SyncStatusQuery, + + // Recommendation DTOs + v1::dto::recommendations::RecommendationDto, + v1::dto::recommendations::RecommendationsResponse, + v1::dto::recommendations::RecommendationsRefreshResponse, + v1::dto::recommendations::DismissRecommendationRequest, + v1::dto::recommendations::DismissRecommendationResponse, + // Plugin Actions DTOs v1::dto::PluginActionDto, v1::dto::PluginActionsResponse, @@ -904,6 +942,8 @@ The following paths are exempt from rate limiting: (name = "Settings", description = "Runtime configuration settings (admin only)"), (name = "Plugins", description = "Admin-managed external plugin processes"), (name = "Plugin Actions", description = "Plugin action discovery and execution for metadata fetching"), + (name = "User Plugins", description = "User-facing plugin management, OAuth, and configuration"), + (name = "Recommendations", description = "Personalized recommendation endpoints"), (name = "Metrics", description = "Application metrics and statistics"), (name = "Filesystem", description = "Filesystem browsing for library paths"), (name = "Duplicates", description = "Duplicate book detection and management"), @@ -1028,7 +1068,7 @@ impl utoipa::Modify for TagGroupsModifier { }, { "name": "User Features", - "tags": ["Users", "User Preferences", "Reading Progress"] + "tags": ["Users", "User Preferences", "User Plugins", "Recommendations", "Reading Progress"] }, { "name": "Background Jobs", diff --git a/src/api/error.rs b/src/api/error.rs index eeb7403e..6093b9fd 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -15,6 +15,8 @@ pub enum ApiError { Conflict(String), /// Resource exists but cannot be processed (e.g., PDF without PDFium) UnprocessableEntity(String), + /// Too many requests — rate limit exceeded + TooManyRequests(String), /// Service is unavailable due to missing configuration or dependencies ServiceUnavailable(String), Internal(String), @@ -63,6 +65,10 @@ impl IntoResponse for ApiError { tracing::debug!(error = "UnprocessableEntity", message = %msg, "Unprocessable entity"); (StatusCode::UNPROCESSABLE_ENTITY, "UnprocessableEntity", msg) } + ApiError::TooManyRequests(msg) => { + tracing::debug!(error = "TooManyRequests", message = %msg, "Rate limit exceeded"); + (StatusCode::TOO_MANY_REQUESTS, "TooManyRequests", msg) + } ApiError::ServiceUnavailable(msg) => { // Log at warn level - server configuration issue tracing::warn!(error = "ServiceUnavailable", message = %msg, "Service unavailable"); diff --git a/src/api/extractors/auth.rs b/src/api/extractors/auth.rs index 0c045506..c21a59b4 100644 --- a/src/api/extractors/auth.rs +++ b/src/api/extractors/auth.rs @@ -216,6 +216,8 @@ pub struct AppState { /// OIDC authentication service for external identity provider authentication /// None when OIDC is disabled in config pub oidc_service: Option>, + /// OAuth state manager for user plugin OAuth flows + pub oauth_state_manager: Arc, } // Legacy alias for backwards compatibility during transition diff --git a/src/api/routes/v1/dto/mod.rs b/src/api/routes/v1/dto/mod.rs index c909cd9e..2ddd874d 100644 --- a/src/api/routes/v1/dto/mod.rs +++ b/src/api/routes/v1/dto/mod.rs @@ -18,6 +18,7 @@ pub mod patch; pub mod pdf_cache; pub mod plugins; pub mod read_progress; +pub mod recommendations; pub mod scan; pub mod series; pub mod settings; @@ -25,6 +26,7 @@ pub mod setup; pub mod sharing_tag; pub mod task_metrics; pub mod user; +pub mod user_plugins; pub mod user_preferences; pub use api_key::*; @@ -42,6 +44,8 @@ pub use page::*; pub use pdf_cache::*; pub use plugins::*; pub use read_progress::*; +#[allow(unused_imports)] +pub use recommendations::*; pub use scan::*; pub use series::*; pub use settings::*; @@ -49,4 +53,6 @@ pub use setup::*; pub use sharing_tag::*; pub use task_metrics::*; pub use user::*; +#[allow(unused_imports)] +pub use user_plugins::*; pub use user_preferences::*; diff --git a/src/api/routes/v1/dto/plugins.rs b/src/api/routes/v1/dto/plugins.rs index 668d863d..4518bdfe 100644 --- a/src/api/routes/v1/dto/plugins.rs +++ b/src/api/routes/v1/dto/plugins.rs @@ -146,6 +146,11 @@ pub struct PluginDto { #[serde(skip_serializing_if = "Option::is_none")] #[schema(example = json!(["series", "book"]))] pub metadata_targets: Option>, + + /// Number of users who have enabled this plugin (only for user-type plugins) + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(example = 3)] + pub user_count: Option, } impl From for PluginDto { @@ -204,6 +209,7 @@ impl From for PluginDto { metadata_targets: model .metadata_targets .and_then(|s| serde_json::from_str(&s).ok()), + user_count: None, } } } @@ -244,6 +250,36 @@ pub struct ConfigSchemaDto { pub fields: Vec, } +/// OAuth 2.0 configuration from plugin manifest +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct OAuthConfigDto { + /// OAuth 2.0 authorization endpoint URL + pub authorization_url: String, + /// OAuth 2.0 token endpoint URL + pub token_url: String, + /// Required OAuth scopes + #[serde(default)] + pub scopes: Vec, + /// Whether to use PKCE (Proof Key for Code Exchange) + pub pkce: bool, + /// Optional user info endpoint URL + #[serde(skip_serializing_if = "Option::is_none")] + pub user_info_url: Option, +} + +impl From for OAuthConfigDto { + fn from(o: crate::services::plugin::protocol::OAuthConfig) -> Self { + Self { + authorization_url: o.authorization_url, + token_url: o.token_url, + scopes: o.scopes, + pkce: o.pkce, + user_info_url: o.user_info_url, + } + } +} + /// Plugin manifest from the plugin itself #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -278,6 +314,15 @@ pub struct PluginManifestDto { /// Configuration schema documenting available config options #[serde(skip_serializing_if = "Option::is_none")] pub config_schema: Option, + /// OAuth 2.0 configuration (if plugin supports OAuth) + #[serde(skip_serializing_if = "Option::is_none")] + pub oauth: Option, + /// Admin-facing setup instructions (e.g., how to create OAuth app, set client ID) + #[serde(skip_serializing_if = "Option::is_none")] + pub admin_setup_instructions: Option, + /// User-facing setup instructions (e.g., how to connect or get a personal token) + #[serde(skip_serializing_if = "Option::is_none")] + pub user_setup_instructions: Option, } impl From for PluginManifestDto { @@ -322,6 +367,9 @@ impl From for PluginManifestD .collect(), scopes, config_schema, + oauth: m.oauth.map(OAuthConfigDto::from), + admin_setup_instructions: m.admin_setup_instructions, + user_setup_instructions: m.user_setup_instructions, } } } @@ -335,7 +383,13 @@ pub struct PluginCapabilitiesDto { pub metadata_provider: Vec, /// Can sync user reading progress #[serde(default)] - pub user_sync_provider: bool, + pub user_read_sync: bool, + /// External ID source for matching sync entries to series (e.g., "api:anilist") + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_id_source: Option, + /// Can provide personalized recommendations + #[serde(default)] + pub user_recommendation_provider: bool, } impl From for PluginCapabilitiesDto { @@ -346,7 +400,9 @@ impl From for PluginCapabilitiesDto { .iter() .map(content_type_to_string) .collect(), - user_sync_provider: c.user_sync_provider, + user_read_sync: c.user_read_sync, + external_id_source: c.external_id_source, + user_recommendation_provider: c.user_recommendation_provider, } } } @@ -528,7 +584,7 @@ fn default_plugin_type() -> String { } fn default_credential_delivery() -> String { - "env".to_string() + "init_message".to_string() } /// Request to update a plugin @@ -1379,7 +1435,7 @@ mod tests { let request: CreatePluginRequest = serde_json::from_value(json).unwrap(); assert_eq!(request.name, "test"); assert_eq!(request.plugin_type, "system"); - assert_eq!(request.credential_delivery, "env"); + assert_eq!(request.credential_delivery, "init_message"); assert!(request.args.is_empty()); assert!(request.permissions.is_empty()); assert!(request.scopes.is_empty()); diff --git a/src/api/routes/v1/dto/recommendations.rs b/src/api/routes/v1/dto/recommendations.rs new file mode 100644 index 00000000..4a289f7a --- /dev/null +++ b/src/api/routes/v1/dto/recommendations.rs @@ -0,0 +1,146 @@ +//! Recommendation DTOs +//! +//! Request and response types for the recommendations API endpoints. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// A single recommendation for the user +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationDto { + /// External ID on the source service + pub external_id: String, + /// URL to the entry on the external service + #[serde(skip_serializing_if = "Option::is_none")] + pub external_url: Option, + /// Title of the recommended series/book + pub title: String, + /// Cover image URL + #[serde(skip_serializing_if = "Option::is_none")] + pub cover_url: Option, + /// Summary/description + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// Genres + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub genres: Vec, + /// Confidence/relevance score (0.0 to 1.0) + pub score: f64, + /// Human-readable reason for this recommendation + pub reason: String, + /// Titles that influenced this recommendation + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub based_on: Vec, + /// Codex series ID if matched to an existing series + #[serde(skip_serializing_if = "Option::is_none")] + pub codex_series_id: Option, + /// Whether this series is already in the user's library + #[serde(default)] + pub in_library: bool, +} + +/// Response from GET /api/v1/user/recommendations +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationsResponse { + /// Personalized recommendations + pub recommendations: Vec, + /// Plugin that provided these recommendations + pub plugin_id: Uuid, + /// Plugin display name + pub plugin_name: String, + /// When these recommendations were generated + #[serde(skip_serializing_if = "Option::is_none")] + pub generated_at: Option, + /// Whether these are cached results + #[serde(default)] + pub cached: bool, +} + +/// Response from POST /api/v1/user/recommendations/refresh +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationsRefreshResponse { + /// Task ID for tracking the refresh operation + pub task_id: Uuid, + /// Human-readable status message + pub message: String, +} + +/// Request body for POST /api/v1/user/recommendations/:id/dismiss +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DismissRecommendationRequest { + /// Reason for dismissal + #[serde(default)] + pub reason: Option, +} + +/// Response from POST /api/v1/user/recommendations/:id/dismiss +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DismissRecommendationResponse { + /// Whether the dismissal was recorded + pub dismissed: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_recommendation_dto_skips_none_fields() { + let dto = RecommendationDto { + external_id: "1".to_string(), + external_url: None, + title: "Test".to_string(), + cover_url: None, + summary: None, + genres: vec![], + score: 0.5, + reason: "test".to_string(), + based_on: vec![], + codex_series_id: None, + in_library: false, + }; + let json = serde_json::to_value(&dto).unwrap(); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("externalUrl")); + assert!(!obj.contains_key("coverUrl")); + assert!(!obj.contains_key("summary")); + assert!(!obj.contains_key("genres")); + assert!(!obj.contains_key("basedOn")); + assert!(!obj.contains_key("codexSeriesId")); + } + + #[test] + fn test_recommendations_response_serialization() { + let resp = RecommendationsResponse { + recommendations: vec![], + plugin_id: Uuid::new_v4(), + plugin_name: "AniList Recs".to_string(), + generated_at: Some("2026-02-06T12:00:00Z".to_string()), + cached: true, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["recommendations"].as_array().unwrap().is_empty()); + assert!(json["cached"].as_bool().unwrap()); + assert_eq!(json["pluginName"], "AniList Recs"); + } + + #[test] + fn test_dismiss_request_with_reason() { + let json = serde_json::json!({ "reason": "not_interested" }); + let req: DismissRecommendationRequest = serde_json::from_value(json).unwrap(); + assert_eq!(req.reason.unwrap(), "not_interested"); + } + + #[test] + fn test_dismiss_request_without_reason() { + let json = serde_json::json!({}); + let req: DismissRecommendationRequest = serde_json::from_value(json).unwrap(); + assert!(req.reason.is_none()); + } +} diff --git a/src/api/routes/v1/dto/user_plugins.rs b/src/api/routes/v1/dto/user_plugins.rs new file mode 100644 index 00000000..a6d6ae4b --- /dev/null +++ b/src/api/routes/v1/dto/user_plugins.rs @@ -0,0 +1,431 @@ +//! User Plugin DTOs +//! +//! Request and response types for user plugin management endpoints. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// OAuth initiation response +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct OAuthStartResponse { + /// The URL to redirect the user to for OAuth authorization + #[schema( + example = "https://anilist.co/api/v2/oauth/authorize?response_type=code&client_id=..." + )] + pub redirect_url: String, +} + +/// OAuth callback query parameters +#[derive(Debug, Deserialize)] +pub struct OAuthCallbackQuery { + /// Authorization code from the OAuth provider + pub code: String, + /// State parameter for CSRF protection + pub state: String, +} + +/// User plugin instance status +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UserPluginDto { + /// User plugin instance ID + pub id: Uuid, + /// Plugin definition ID + pub plugin_id: Uuid, + /// Plugin display name + pub plugin_name: String, + /// Plugin display name for UI + pub plugin_display_name: String, + /// Plugin type: "system" or "user" + pub plugin_type: String, + /// Whether the user has enabled this plugin + pub enabled: bool, + /// Whether the plugin is connected (has valid credentials/OAuth) + pub connected: bool, + /// Health status of this user's plugin instance + pub health_status: String, + /// External service username (if connected via OAuth) + #[serde(skip_serializing_if = "Option::is_none")] + pub external_username: Option, + /// External service avatar URL + #[serde(skip_serializing_if = "Option::is_none")] + pub external_avatar_url: Option, + /// Last sync timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub last_sync_at: Option>, + /// Last successful operation timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub last_success_at: Option>, + /// Whether this plugin requires OAuth authentication + pub requires_oauth: bool, + /// Whether the admin has configured OAuth credentials (client_id set) + pub oauth_configured: bool, + /// User-facing description of the plugin + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// User-facing setup instructions for the plugin + #[serde(skip_serializing_if = "Option::is_none")] + pub user_setup_instructions: Option, + /// Per-user configuration + pub config: serde_json::Value, + /// Plugin capabilities (derived from manifest) + pub capabilities: UserPluginCapabilitiesDto, + /// User-facing configuration schema (from plugin manifest) + #[serde(skip_serializing_if = "Option::is_none")] + pub user_config_schema: Option, + /// Last sync result summary (stored in user_plugin_data) + #[serde(skip_serializing_if = "Option::is_none")] + pub last_sync_result: Option, + /// Created timestamp + pub created_at: DateTime, +} + +/// Available plugin (not yet enabled by user) +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct AvailablePluginDto { + /// Plugin definition ID + pub plugin_id: Uuid, + /// Plugin name + pub name: String, + /// Plugin display name + pub display_name: String, + /// Plugin description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// User-facing setup instructions for the plugin + #[serde(skip_serializing_if = "Option::is_none")] + pub user_setup_instructions: Option, + /// Whether this plugin requires OAuth authentication + pub requires_oauth: bool, + /// Whether the admin has configured OAuth credentials (client_id set) + pub oauth_configured: bool, + /// Plugin capabilities + pub capabilities: UserPluginCapabilitiesDto, +} + +/// Plugin capabilities for display (user plugin context) +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UserPluginCapabilitiesDto { + /// Can sync reading progress + pub read_sync: bool, + /// Can provide recommendations + pub user_recommendation_provider: bool, +} + +/// Request to update user plugin configuration +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateUserPluginConfigRequest { + /// Configuration overrides for this plugin + pub config: serde_json::Value, +} + +/// Request to set user credentials (e.g., personal access token) +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SetUserCredentialsRequest { + /// The access token or API key to store + pub access_token: String, +} + +/// User plugins list response +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UserPluginsListResponse { + /// Plugins the user has enabled + pub enabled: Vec, + /// Plugins available for the user to enable + pub available: Vec, +} + +/// Response from triggering a sync operation +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SyncTriggerResponse { + /// Task ID for tracking the sync operation + pub task_id: Uuid, + /// Human-readable status message + pub message: String, +} + +/// Query parameters for sync status endpoint +#[derive(Debug, Clone, Deserialize, ToSchema, utoipa::IntoParams)] +#[serde(rename_all = "camelCase")] +pub struct SyncStatusQuery { + /// If true, spawn the plugin process and query live sync state + /// (external count, pending push/pull, conflicts). + /// Default: false (returns database-stored metadata only). + #[serde(default)] + pub live: bool, +} + +/// Sync status response for a user plugin +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SyncStatusDto { + /// Plugin ID + pub plugin_id: Uuid, + /// Plugin name + pub plugin_name: String, + /// Whether the plugin is connected and ready to sync + pub connected: bool, + /// Last successful sync timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub last_sync_at: Option>, + /// Last successful operation timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub last_success_at: Option>, + /// Last failure timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub last_failure_at: Option>, + /// Health status + pub health_status: String, + /// Number of consecutive failures + pub failure_count: i32, + /// Whether the plugin is currently enabled + pub enabled: bool, + /// Number of entries tracked on the external service (only with `?live=true`) + #[serde(skip_serializing_if = "Option::is_none")] + pub external_count: Option, + /// Number of local entries that need to be pushed (only with `?live=true`) + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_push: Option, + /// Number of external entries that need to be pulled (only with `?live=true`) + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_pull: Option, + /// Number of entries with conflicts on both sides (only with `?live=true`) + #[serde(skip_serializing_if = "Option::is_none")] + pub conflicts: Option, + /// Error message if `?live=true` was requested but the plugin could not be queried + #[serde(skip_serializing_if = "Option::is_none")] + pub live_error: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sync_status_dto_omits_live_fields_when_none() { + let dto = SyncStatusDto { + plugin_id: Uuid::new_v4(), + plugin_name: "AniList".to_string(), + connected: true, + last_sync_at: None, + last_success_at: None, + last_failure_at: None, + health_status: "healthy".to_string(), + failure_count: 0, + enabled: true, + external_count: None, + pending_push: None, + pending_pull: None, + conflicts: None, + live_error: None, + }; + let json = serde_json::to_value(&dto).unwrap(); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("externalCount")); + assert!(!obj.contains_key("pendingPush")); + assert!(!obj.contains_key("pendingPull")); + assert!(!obj.contains_key("conflicts")); + assert!(!obj.contains_key("liveError")); + } + + #[test] + fn test_sync_status_dto_includes_live_fields_when_present() { + let dto = SyncStatusDto { + plugin_id: Uuid::new_v4(), + plugin_name: "AniList".to_string(), + connected: true, + last_sync_at: None, + last_success_at: None, + last_failure_at: None, + health_status: "healthy".to_string(), + failure_count: 0, + enabled: true, + external_count: Some(150), + pending_push: Some(5), + pending_pull: Some(3), + conflicts: Some(1), + live_error: None, + }; + let json = serde_json::to_value(&dto).unwrap(); + assert_eq!(json["externalCount"], 150); + assert_eq!(json["pendingPush"], 5); + assert_eq!(json["pendingPull"], 3); + assert_eq!(json["conflicts"], 1); + assert!(!json.as_object().unwrap().contains_key("liveError")); + } + + #[test] + fn test_sync_status_dto_includes_live_error() { + let dto = SyncStatusDto { + plugin_id: Uuid::new_v4(), + plugin_name: "AniList".to_string(), + connected: false, + last_sync_at: None, + last_success_at: None, + last_failure_at: None, + health_status: "unknown".to_string(), + failure_count: 0, + enabled: true, + external_count: None, + pending_push: None, + pending_pull: None, + conflicts: None, + live_error: Some("Plugin unavailable: not found".to_string()), + }; + let json = serde_json::to_value(&dto).unwrap(); + assert!( + json["liveError"] + .as_str() + .unwrap() + .contains("Plugin unavailable") + ); + assert!(!json.as_object().unwrap().contains_key("externalCount")); + } + + #[test] + fn test_sync_status_query_defaults_to_false() { + let query: SyncStatusQuery = serde_json::from_value(serde_json::json!({})).unwrap(); + assert!(!query.live); + } + + #[test] + fn test_sync_status_query_live_true() { + let query: SyncStatusQuery = + serde_json::from_value(serde_json::json!({"live": true})).unwrap(); + assert!(query.live); + } + + #[test] + fn test_user_plugin_dto_includes_capabilities() { + let dto = UserPluginDto { + id: Uuid::new_v4(), + plugin_id: Uuid::new_v4(), + plugin_name: "sync-anilist".to_string(), + plugin_display_name: "AniList Sync".to_string(), + plugin_type: "user".to_string(), + enabled: true, + connected: true, + health_status: "healthy".to_string(), + external_username: None, + external_avatar_url: None, + last_sync_at: None, + last_success_at: None, + requires_oauth: true, + oauth_configured: true, + description: None, + user_setup_instructions: None, + config: serde_json::json!({}), + capabilities: UserPluginCapabilitiesDto { + read_sync: true, + user_recommendation_provider: false, + }, + user_config_schema: None, + last_sync_result: None, + created_at: chrono::Utc::now(), + }; + let json = serde_json::to_value(&dto).unwrap(); + assert_eq!(json["capabilities"]["readSync"], true); + assert_eq!(json["capabilities"]["userRecommendationProvider"], false); + assert!(!json.as_object().unwrap().contains_key("userConfigSchema")); + assert!(!json.as_object().unwrap().contains_key("lastSyncResult")); + } + + #[test] + fn test_user_plugin_dto_includes_user_config_schema() { + let schema = super::super::plugins::ConfigSchemaDto { + description: Some("Test config".to_string()), + fields: vec![super::super::plugins::ConfigFieldDto { + key: "scoreFormat".to_string(), + label: "Score Format".to_string(), + description: Some("How scores are mapped".to_string()), + field_type: "string".to_string(), + required: false, + default: Some(serde_json::json!("POINT_10")), + example: None, + }], + }; + + let dto = UserPluginDto { + id: Uuid::new_v4(), + plugin_id: Uuid::new_v4(), + plugin_name: "sync-anilist".to_string(), + plugin_display_name: "AniList Sync".to_string(), + plugin_type: "user".to_string(), + enabled: true, + connected: true, + health_status: "healthy".to_string(), + external_username: None, + external_avatar_url: None, + last_sync_at: None, + last_success_at: None, + requires_oauth: true, + oauth_configured: true, + description: None, + user_setup_instructions: None, + config: serde_json::json!({}), + capabilities: UserPluginCapabilitiesDto { + read_sync: true, + user_recommendation_provider: false, + }, + user_config_schema: Some(schema), + last_sync_result: None, + created_at: chrono::Utc::now(), + }; + let json = serde_json::to_value(&dto).unwrap(); + let schema_json = &json["userConfigSchema"]; + assert_eq!(schema_json["description"], "Test config"); + assert_eq!(schema_json["fields"][0]["key"], "scoreFormat"); + assert_eq!(schema_json["fields"][0]["label"], "Score Format"); + } + + #[test] + fn test_user_plugin_dto_includes_last_sync_result() { + let sync_result = serde_json::json!({ + "pulled": 10, + "matched": 8, + "applied": 6, + "pushed": 5, + "pushFailures": 0, + }); + + let dto = UserPluginDto { + id: Uuid::new_v4(), + plugin_id: Uuid::new_v4(), + plugin_name: "sync-anilist".to_string(), + plugin_display_name: "AniList Sync".to_string(), + plugin_type: "user".to_string(), + enabled: true, + connected: true, + health_status: "healthy".to_string(), + external_username: None, + external_avatar_url: None, + last_sync_at: None, + last_success_at: None, + requires_oauth: true, + oauth_configured: true, + description: None, + user_setup_instructions: None, + config: serde_json::json!({}), + capabilities: UserPluginCapabilitiesDto { + read_sync: true, + user_recommendation_provider: false, + }, + user_config_schema: None, + last_sync_result: Some(sync_result.clone()), + created_at: chrono::Utc::now(), + }; + let json = serde_json::to_value(&dto).unwrap(); + assert_eq!(json["lastSyncResult"]["pulled"], 10); + assert_eq!(json["lastSyncResult"]["applied"], 6); + assert_eq!(json["lastSyncResult"]["pushed"], 5); + } +} diff --git a/src/api/routes/v1/handlers/mod.rs b/src/api/routes/v1/handlers/mod.rs index d6aa0df7..00d2f48b 100644 --- a/src/api/routes/v1/handlers/mod.rs +++ b/src/api/routes/v1/handlers/mod.rs @@ -57,6 +57,7 @@ pub mod pdf_cache; pub mod plugin_actions; pub mod plugins; pub mod read_progress; +pub mod recommendations; pub mod scan; pub mod series; pub mod settings; @@ -64,6 +65,7 @@ pub mod setup; pub mod sharing_tags; pub mod task_metrics; pub mod task_queue; +pub mod user_plugins; pub mod user_preferences; pub mod users; diff --git a/src/api/routes/v1/handlers/plugin_actions.rs b/src/api/routes/v1/handlers/plugin_actions.rs index a576593b..8fb845a7 100644 --- a/src/api/routes/v1/handlers/plugin_actions.rs +++ b/src/api/routes/v1/handlers/plugin_actions.rs @@ -43,8 +43,9 @@ use axum::{ Json, extract::{Path, Query, State}, }; +use sea_orm::prelude::Decimal; use serde::Deserialize; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Instant; use utoipa::OpenApi; @@ -765,10 +766,15 @@ pub async fn preview_series_metadata( )); // Alternate Titles - let current_alt_titles: Vec = current_alternate_titles + let mut current_alt_titles: Vec = current_alternate_titles .iter() .map(|t| serde_json::json!({"label": t.label, "title": t.title})) .collect(); + current_alt_titles.sort_by(|a, b| { + let a_title = a["title"].as_str().unwrap_or(""); + let b_title = b["title"].as_str().unwrap_or(""); + a_title.cmp(b_title) + }); fields.push(build_field_preview( "alternateTitles", if current_alt_titles.is_empty() { @@ -779,16 +785,36 @@ pub async fn preview_series_metadata( if plugin_metadata.alternate_titles.is_empty() { None } else { - let proposed_alt_titles: Vec = plugin_metadata + // Generate labels matching the apply logic: language > title_type > "alternate", + // with dedup suffixes for duplicates (e.g., "en", "en-2", "en-3") + let mut label_counts: HashMap = HashMap::new(); + let mut proposed_alt_titles: Vec = plugin_metadata .alternate_titles .iter() .map(|t| { + let base_label = t + .language + .clone() + .or_else(|| t.title_type.clone()) + .unwrap_or_else(|| "alternate".to_string()); + let count = label_counts.entry(base_label.clone()).or_insert(0); + *count += 1; + let label = if *count == 1 { + base_label + } else { + format!("{}-{}", base_label, count) + }; serde_json::json!({ - "label": t.title_type.as_deref().unwrap_or("Alternative"), + "label": label, "title": t.title }) }) .collect(); + proposed_alt_titles.sort_by(|a, b| { + let a_title = a["title"].as_str().unwrap_or(""); + let b_title = b["title"].as_str().unwrap_or(""); + a_title.cmp(b_title) + }); Some(serde_json::json!(proposed_alt_titles)) }, current_metadata @@ -889,14 +915,18 @@ pub async fn preview_series_metadata( )); // Genres - let current_genre_names: Vec = current_genres.iter().map(|g| g.name.clone()).collect(); + let mut current_genre_names: Vec = + current_genres.iter().map(|g| g.name.clone()).collect(); + current_genre_names.sort(); fields.push(build_field_preview( "genres", Some(serde_json::json!(current_genre_names)), if plugin_metadata.genres.is_empty() { None } else { - Some(serde_json::json!(plugin_metadata.genres)) + let mut proposed_genres = plugin_metadata.genres.clone(); + proposed_genres.sort(); + Some(serde_json::json!(proposed_genres)) }, current_metadata .as_ref() @@ -911,14 +941,17 @@ pub async fn preview_series_metadata( )); // Tags - let current_tag_names: Vec = current_tags.iter().map(|t| t.name.clone()).collect(); + let mut current_tag_names: Vec = current_tags.iter().map(|t| t.name.clone()).collect(); + current_tag_names.sort(); fields.push(build_field_preview( "tags", Some(serde_json::json!(current_tag_names)), if plugin_metadata.tags.is_empty() { None } else { - Some(serde_json::json!(plugin_metadata.tags)) + let mut proposed_tags = plugin_metadata.tags.clone(); + proposed_tags.sort(); + Some(serde_json::json!(proposed_tags)) }, current_metadata .as_ref() @@ -1021,21 +1054,46 @@ pub async fn preview_series_metadata( let current_links = ExternalLinkRepository::get_for_series(&state.db, series_id) .await .map_err(|e| ApiError::Internal(format!("Failed to get external links: {}", e)))?; - let current_link_sources: Vec = current_links + // Build set of normalized source names the plugin provides, to filter current links + let proposed_link_sources: HashSet = plugin_metadata + .external_links .iter() - .map(|l| l.source_name.clone()) + .map(|l| l.label.to_lowercase().trim().to_string()) .collect(); + // Filter current links to only include sources the plugin provides + let mut current_link_values: Vec = current_links + .iter() + .filter(|l| proposed_link_sources.contains(&l.source_name)) + .map(|l| serde_json::json!({"label": l.source_name.clone(), "url": l.url.clone()})) + .collect(); + current_link_values.sort_by(|a, b| { + let a_label = a["label"].as_str().unwrap_or(""); + let b_label = b["label"].as_str().unwrap_or(""); + a_label.cmp(b_label) + }); fields.push(build_field_preview( "externalLinks", - Some(serde_json::json!(current_link_sources)), + if current_link_values.is_empty() { + None + } else { + Some(serde_json::json!(current_link_values)) + }, if plugin_metadata.external_links.is_empty() { None } else { - let proposed_links: Vec = plugin_metadata + let mut proposed_links: Vec = plugin_metadata .external_links .iter() - .map(|l| serde_json::json!({"label": l.label, "url": l.url})) + .map(|l| { + // Normalize label to lowercase to match DB storage + serde_json::json!({"label": l.label.to_lowercase().trim(), "url": l.url.trim()}) + }) .collect(); + proposed_links.sort_by(|a, b| { + let a_label = a["label"].as_str().unwrap_or(""); + let b_label = b["label"].as_str().unwrap_or(""); + a_label.cmp(b_label) + }); Some(serde_json::json!(proposed_links)) }, false, // Links don't have a lock field @@ -1054,7 +1112,10 @@ pub async fn preview_series_metadata( let current_rating_info: Option = current_ratings .iter() .find(|r| r.source_name == plugin.name.to_lowercase()) - .map(|r| serde_json::json!({"score": r.rating, "source": r.source_name})); + .map(|r| { + let score: f64 = Decimal::to_string(&r.rating).parse().unwrap_or(0.0); + serde_json::json!({"score": score, "voteCount": r.vote_count, "source": r.source_name}) + }); fields.push(build_field_preview( "rating", current_rating_info, @@ -1076,12 +1137,27 @@ pub async fn preview_series_metadata( // External Ratings array (multiple sources like AniList, MAL, etc.) if !plugin_metadata.external_ratings.is_empty() { - // Build current ratings map for comparison - let current_ext_ratings: Vec = current_ratings + // Build set of sources the plugin is providing, so we only compare those + let proposed_sources: HashSet<&str> = plugin_metadata + .external_ratings + .iter() + .map(|r| r.source.as_str()) + .collect(); + // Filter current ratings to only include sources the plugin provides + let mut current_ext_ratings: Vec = current_ratings .iter() - .map(|r| serde_json::json!({"score": r.rating, "source": r.source_name})) + .filter(|r| proposed_sources.contains(r.source_name.as_str())) + .map(|r| { + let score: f64 = Decimal::to_string(&r.rating).parse().unwrap_or(0.0); + serde_json::json!({"score": score, "voteCount": r.vote_count, "source": r.source_name}) + }) .collect(); - let proposed_ext_ratings: Vec = plugin_metadata + current_ext_ratings.sort_by(|a, b| { + let a_src = a["source"].as_str().unwrap_or(""); + let b_src = b["source"].as_str().unwrap_or(""); + a_src.cmp(b_src) + }); + let mut proposed_ext_ratings: Vec = plugin_metadata .external_ratings .iter() .map(|r| { @@ -1092,6 +1168,11 @@ pub async fn preview_series_metadata( }) }) .collect(); + proposed_ext_ratings.sort_by(|a, b| { + let a_src = a["source"].as_str().unwrap_or(""); + let b_src = b["source"].as_str().unwrap_or(""); + a_src.cmp(b_src) + }); fields.push(build_field_preview( "externalRatings", if current_ext_ratings.is_empty() { @@ -1130,6 +1211,65 @@ pub async fn preview_series_metadata( &mut not_provided, )); + // External IDs (cross-reference IDs from other services like AniList, MAL) + if !plugin_metadata.external_ids.is_empty() { + let current_ext_ids = SeriesExternalIdRepository::get_for_series(&state.db, series_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get external IDs: {}", e)))?; + // Filter current external IDs to only include sources the plugin provides + let proposed_ext_id_sources: HashSet<&str> = plugin_metadata + .external_ids + .iter() + .map(|e| e.source.as_str()) + .collect(); + let mut current_ext_id_sources: Vec = current_ext_ids + .iter() + .filter(|e| proposed_ext_id_sources.contains(e.source.as_str())) + .map(|e| { + serde_json::json!({ + "source": e.source, + "externalId": e.external_id + }) + }) + .collect(); + current_ext_id_sources.sort_by(|a, b| { + let a_src = a["source"].as_str().unwrap_or(""); + let b_src = b["source"].as_str().unwrap_or(""); + a_src.cmp(b_src) + }); + let mut proposed_ext_ids: Vec = plugin_metadata + .external_ids + .iter() + .map(|e| { + serde_json::json!({ + "source": e.source, + "externalId": e.external_id + }) + }) + .collect(); + proposed_ext_ids.sort_by(|a, b| { + let a_src = a["source"].as_str().unwrap_or(""); + let b_src = b["source"].as_str().unwrap_or(""); + a_src.cmp(b_src) + }); + fields.push(build_field_preview( + "externalIds", + if current_ext_id_sources.is_empty() { + None + } else { + Some(serde_json::json!(current_ext_id_sources)) + }, + Some(serde_json::json!(proposed_ext_ids)), + false, // External IDs don't have a lock field + has_permission(PluginPermission::MetadataWriteExternalIds), + &mut will_apply, + &mut locked, + &mut no_permission, + &mut unchanged, + &mut not_provided, + )); + } + Ok(Json(MetadataPreviewResponse { fields, summary: PreviewSummary { @@ -2202,9 +2342,6 @@ fn sanitize_plugin_error(error: &PluginManagerError) -> String { // User-actionable errors - return sanitized messages PluginManagerError::PluginNotFound(id) => format!("Plugin {} not found", id), PluginManagerError::PluginNotEnabled(id) => format!("Plugin {} is not enabled", id), - PluginManagerError::NoPluginsForScope(_) => { - "No plugins available for this operation".to_string() - } PluginManagerError::RateLimited { .. } => { "Plugin rate limit exceeded, please try again later".to_string() } @@ -2212,6 +2349,19 @@ fn sanitize_plugin_error(error: &PluginManagerError) -> String { // Nested plugin errors - extract and sanitize PluginManagerError::Plugin(plugin_error) => sanitize_nested_plugin_error(plugin_error), + // User plugin errors + PluginManagerError::UserPluginNotFound { .. } => { + "User plugin connection not found".to_string() + } + + // OAuth token errors - user-actionable + PluginManagerError::ReauthRequired(_) => { + "OAuth session expired. Please reconnect the plugin.".to_string() + } + PluginManagerError::TokenRefreshFailed(_) => { + "Failed to refresh OAuth token. Please try again or reconnect.".to_string() + } + // Internal errors - don't expose details PluginManagerError::Database(_) | PluginManagerError::Encryption(_) => { "An internal plugin error occurred".to_string() @@ -2230,11 +2380,9 @@ fn sanitize_nested_plugin_error(error: &crate::services::plugin::handle::PluginE match error { PluginError::NotInitialized => "Plugin is not ready, please try again".to_string(), PluginError::Disabled { .. } => "Plugin is disabled".to_string(), - PluginError::HealthCheckFailed(_) => "Plugin is temporarily unavailable".to_string(), PluginError::SpawnFailed(_) => { "Failed to start plugin, please contact an administrator".to_string() } - PluginError::InvalidManifest(_) => "Plugin configuration error".to_string(), // RPC errors - these may contain more detail PluginError::Rpc(rpc_error) => match rpc_error { @@ -2741,4 +2889,142 @@ mod tests { assert_eq!(preview.status, FieldApplyStatus::NotProvided); assert_eq!(not_provided, 1); } + + #[test] + fn test_build_field_preview_sorted_arrays_are_unchanged() { + let mut will_apply = 0; + let mut locked = 0; + let mut no_permission = 0; + let mut unchanged = 0; + let mut not_provided = 0; + + // Simulate what happens after sorting: same genres in same order should match + let mut current = vec!["Drama".to_string(), "Action".to_string()]; + current.sort(); + let mut proposed = vec!["Action".to_string(), "Drama".to_string()]; + proposed.sort(); + + let preview = build_field_preview( + "genres", + Some(serde_json::json!(current)), + Some(serde_json::json!(proposed)), + false, + true, + &mut will_apply, + &mut locked, + &mut no_permission, + &mut unchanged, + &mut not_provided, + ); + + assert_eq!(preview.status, FieldApplyStatus::Unchanged); + assert_eq!(unchanged, 1); + } + + #[test] + fn test_build_field_preview_sorted_objects_are_unchanged() { + let mut will_apply = 0; + let mut locked = 0; + let mut no_permission = 0; + let mut unchanged = 0; + let mut not_provided = 0; + + // Simulate sorted external links with same structure + let mut current = vec![ + serde_json::json!({"label": "Kitsu", "url": "https://kitsu.app/manga/123"}), + serde_json::json!({"label": "AniList", "url": "https://anilist.co/manga/456"}), + ]; + current.sort_by(|a, b| { + a["label"] + .as_str() + .unwrap_or("") + .cmp(b["label"].as_str().unwrap_or("")) + }); + + let mut proposed = vec![ + serde_json::json!({"label": "AniList", "url": "https://anilist.co/manga/456"}), + serde_json::json!({"label": "Kitsu", "url": "https://kitsu.app/manga/123"}), + ]; + proposed.sort_by(|a, b| { + a["label"] + .as_str() + .unwrap_or("") + .cmp(b["label"].as_str().unwrap_or("")) + }); + + let preview = build_field_preview( + "externalLinks", + Some(serde_json::json!(current)), + Some(serde_json::json!(proposed)), + false, + true, + &mut will_apply, + &mut locked, + &mut no_permission, + &mut unchanged, + &mut not_provided, + ); + + assert_eq!(preview.status, FieldApplyStatus::Unchanged); + assert_eq!(unchanged, 1); + } + + #[test] + fn test_build_field_preview_ratings_with_same_structure_are_unchanged() { + let mut will_apply = 0; + let mut locked = 0; + let mut no_permission = 0; + let mut unchanged = 0; + let mut not_provided = 0; + + // Both current and proposed use the same structure (score as f64, voteCount, source) + let current = serde_json::json!({"score": 79.5, "voteCount": 100, "source": "mangabaka"}); + let proposed = serde_json::json!({"score": 79.5, "voteCount": 100, "source": "mangabaka"}); + + let preview = build_field_preview( + "rating", + Some(current), + Some(proposed), + false, + true, + &mut will_apply, + &mut locked, + &mut no_permission, + &mut unchanged, + &mut not_provided, + ); + + assert_eq!(preview.status, FieldApplyStatus::Unchanged); + assert_eq!(unchanged, 1); + } + + #[test] + fn test_build_field_preview_mismatched_structure_shows_will_apply() { + let mut will_apply = 0; + let mut locked = 0; + let mut no_permission = 0; + let mut unchanged = 0; + let mut not_provided = 0; + + // Old bug: current lacked voteCount, proposed had it — this should detect the difference + let current = serde_json::json!({"score": 79.5, "source": "mangabaka"}); + let proposed = serde_json::json!({"score": 79.5, "voteCount": 100, "source": "mangabaka"}); + + let preview = build_field_preview( + "rating", + Some(current), + Some(proposed), + false, + true, + &mut will_apply, + &mut locked, + &mut no_permission, + &mut unchanged, + &mut not_provided, + ); + + // These are structurally different, so it should detect a change + assert_eq!(preview.status, FieldApplyStatus::WillApply); + assert_eq!(will_apply, 1); + } } diff --git a/src/api/routes/v1/handlers/plugins.rs b/src/api/routes/v1/handlers/plugins.rs index ff03500a..825b4fd0 100644 --- a/src/api/routes/v1/handlers/plugins.rs +++ b/src/api/routes/v1/handlers/plugins.rs @@ -12,7 +12,7 @@ use super::super::dto::{ }; use crate::api::{AppState, error::ApiError, extractors::AuthContext, permissions::Permission}; use crate::db::entities::plugins::PluginPermission; -use crate::db::repositories::{PluginFailuresRepository, PluginsRepository}; +use crate::db::repositories::{PluginFailuresRepository, PluginsRepository, UserPluginsRepository}; use crate::events::{EntityChangeEvent, EntityEvent}; use crate::services::PluginHealthStatus; use crate::services::plugin::process::{allowed_commands_description, is_command_allowed}; @@ -88,8 +88,23 @@ pub async fn list_plugins( .await .map_err(|e| ApiError::Internal(format!("Failed to get plugins: {}", e)))?; + let user_counts = UserPluginsRepository::count_users_per_plugin(&state.db) + .await + .map_err(|e| ApiError::Internal(format!("Failed to count plugin users: {}", e)))?; + let total = plugins.len(); - let dtos: Vec = plugins.into_iter().map(Into::into).collect(); + let dtos: Vec = plugins + .into_iter() + .map(|p| { + let id = p.id; + let is_user_plugin = p.plugin_type == "user"; + let mut dto: PluginDto = p.into(); + if is_user_plugin { + dto.user_count = Some(*user_counts.get(&id).unwrap_or(&0)); + } + dto + }) + .collect(); Ok(Json(PluginsListResponse { plugins: dtos, diff --git a/src/api/routes/v1/handlers/recommendations.rs b/src/api/routes/v1/handlers/recommendations.rs new file mode 100644 index 00000000..cabedfce --- /dev/null +++ b/src/api/routes/v1/handlers/recommendations.rs @@ -0,0 +1,485 @@ +//! Recommendation Handlers +//! +//! Handlers for personalized recommendation endpoints. +//! These endpoints allow users to get recommendations from plugins, +//! refresh cached recommendations, and dismiss individual suggestions. + +use super::super::dto::recommendations::{ + DismissRecommendationRequest, DismissRecommendationResponse, RecommendationDto, + RecommendationsRefreshResponse, RecommendationsResponse, +}; +use crate::api::extractors::auth::AuthContext; +use crate::api::{error::ApiError, extractors::AppState}; +use crate::db::repositories::{PluginsRepository, TaskRepository, UserPluginsRepository}; +use crate::services::plugin::library::build_user_library; +use crate::services::plugin::protocol::{PluginManifest, methods}; +use crate::services::plugin::recommendations::{ + RecommendationDismissRequest, RecommendationRequest, RecommendationResponse, +}; +use crate::tasks::types::TaskType; +use axum::{ + Json, + extract::{Path, State}, +}; +use std::sync::Arc; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +/// Find the user's recommendation plugin. +/// +/// Returns the plugin definition and user plugin instance for the first enabled +/// recommendation provider plugin the user has connected. +async fn find_recommendation_plugin( + db: &sea_orm::DatabaseConnection, + user_id: Uuid, +) -> Result< + ( + crate::db::entities::plugins::Model, + crate::db::entities::user_plugins::Model, + ), + ApiError, +> { + let user_instances = UserPluginsRepository::get_enabled_for_user(db, user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get user plugins: {}", e)))?; + + for instance in user_instances { + let plugin = PluginsRepository::get_by_id(db, instance.plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))?; + + if let Some(plugin) = plugin { + let is_rec_provider = plugin + .manifest + .as_ref() + .and_then(|m| serde_json::from_value::(m.clone()).ok()) + .map(|m| m.capabilities.user_recommendation_provider) + .unwrap_or(false); + + if is_rec_provider { + return Ok((plugin, instance)); + } + } + } + + Err(ApiError::NotFound( + "No recommendation plugin enabled. Enable a recommendation plugin in Settings > Integrations." + .to_string(), + )) +} + +/// Get personalized recommendations +/// +/// Returns recommendations from the user's enabled recommendation plugin. +/// The plugin may return cached results or generate fresh recommendations. +#[utoipa::path( + get, + path = "/api/v1/user/recommendations", + responses( + (status = 200, description = "Personalized recommendations", body = RecommendationsResponse), + (status = 401, description = "Not authenticated"), + (status = 404, description = "No recommendation plugin enabled"), + ), + tag = "Recommendations" +)] +pub async fn get_recommendations( + State(state): State>, + auth: AuthContext, +) -> Result, ApiError> { + let (plugin, _instance) = find_recommendation_plugin(&state.db, auth.user_id).await?; + + debug!( + user_id = %auth.user_id, + plugin_id = %plugin.id, + "Fetching recommendations from plugin" + ); + + // Spawn plugin and call recommendations/get + let (handle, _context) = state + .plugin_manager + .get_user_plugin_handle(plugin.id, auth.user_id) + .await + .map_err(|e| { + warn!( + plugin_id = %plugin.id, + error = %e, + "Failed to spawn recommendation plugin" + ); + ApiError::Internal(format!("Failed to start recommendation plugin: {}", e)) + })?; + + // Build user's library data to seed recommendations + let library = build_user_library(&state.db, auth.user_id) + .await + .map_err(|e| { + warn!( + user_id = %auth.user_id, + error = %e, + "Failed to build user library for recommendations" + ); + ApiError::Internal(format!("Failed to build library data: {}", e)) + })?; + + debug!( + user_id = %auth.user_id, + library_entries = library.len(), + "Sending library data to recommendation plugin" + ); + + let request = RecommendationRequest { + library, + limit: Some(20), + exclude_ids: vec![], + }; + + let response = handle + .call_method::( + methods::RECOMMENDATIONS_GET, + request, + ) + .await + .map_err(|e| { + warn!( + plugin_id = %plugin.id, + error = %e, + "Failed to get recommendations from plugin" + ); + ApiError::Internal(format!("Recommendation plugin error: {}", e)) + })?; + + // Convert plugin response to API DTO + let recommendations = response + .recommendations + .into_iter() + .map(to_recommendation_dto) + .collect(); + + Ok(Json(RecommendationsResponse { + recommendations, + plugin_id: plugin.id, + plugin_name: plugin.display_name.clone(), + generated_at: response.generated_at, + cached: response.cached, + })) +} + +/// Refresh recommendations +/// +/// Enqueues a background task to regenerate recommendations by clearing +/// the cache and updating the taste profile. +#[utoipa::path( + post, + path = "/api/v1/user/recommendations/refresh", + responses( + (status = 200, description = "Refresh task enqueued", body = RecommendationsRefreshResponse), + (status = 401, description = "Not authenticated"), + (status = 404, description = "No recommendation plugin enabled"), + (status = 409, description = "Recommendation refresh already in progress"), + ), + tag = "Recommendations" +)] +pub async fn refresh_recommendations( + State(state): State>, + auth: AuthContext, +) -> Result, ApiError> { + let (plugin, _instance) = find_recommendation_plugin(&state.db, auth.user_id).await?; + + // Check for duplicate pending/processing recommendation task + let has_existing = TaskRepository::has_pending_or_processing( + &state.db, + "user_plugin_recommendations", + plugin.id, + auth.user_id, + ) + .await + .map_err(|e| ApiError::Internal(format!("Failed to check existing tasks: {}", e)))?; + + if has_existing { + return Err(ApiError::Conflict( + "Recommendation refresh already in progress".to_string(), + )); + } + + let task_type = TaskType::UserPluginRecommendations { + plugin_id: plugin.id, + user_id: auth.user_id, + }; + + let task_id = TaskRepository::enqueue(&state.db, task_type, 0, None) + .await + .map_err(|e| { + ApiError::Internal(format!("Failed to enqueue recommendations task: {}", e)) + })?; + + info!( + user_id = %auth.user_id, + plugin_id = %plugin.id, + task_id = %task_id, + "Enqueued recommendations refresh task" + ); + + Ok(Json(RecommendationsRefreshResponse { + task_id, + message: format!("Refreshing recommendations from {}", plugin.display_name), + })) +} + +/// Convert a plugin Recommendation to an API RecommendationDto +/// +/// This is extracted for testability — the handler maps the plugin's response +/// into the API response type field-by-field. +fn to_recommendation_dto( + r: crate::services::plugin::recommendations::Recommendation, +) -> RecommendationDto { + RecommendationDto { + external_id: r.external_id, + external_url: r.external_url, + title: r.title, + cover_url: r.cover_url, + summary: r.summary, + genres: r.genres, + score: r.score, + reason: r.reason, + based_on: r.based_on, + codex_series_id: r.codex_series_id, + in_library: r.in_library, + } +} + +/// Dismiss a recommendation +/// +/// Tells the recommendation plugin that the user is not interested in a +/// particular recommendation, so it can be excluded from future results. +#[utoipa::path( + post, + path = "/api/v1/user/recommendations/{external_id}/dismiss", + params( + ("external_id" = String, Path, description = "External ID of the recommendation to dismiss") + ), + request_body = DismissRecommendationRequest, + responses( + (status = 200, description = "Recommendation dismissed", body = DismissRecommendationResponse), + (status = 401, description = "Not authenticated"), + (status = 404, description = "No recommendation plugin enabled"), + ), + tag = "Recommendations" +)] +pub async fn dismiss_recommendation( + State(state): State>, + auth: AuthContext, + Path(external_id): Path, + Json(request): Json, +) -> Result, ApiError> { + let (plugin, _instance) = find_recommendation_plugin(&state.db, auth.user_id).await?; + + debug!( + user_id = %auth.user_id, + plugin_id = %plugin.id, + external_id = %external_id, + "Dismissing recommendation" + ); + + // Spawn plugin and call recommendations/dismiss + let (handle, _context) = state + .plugin_manager + .get_user_plugin_handle(plugin.id, auth.user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to start recommendation plugin: {}", e)))?; + + let dismiss_request = RecommendationDismissRequest { + external_id: external_id.clone(), + reason: request.reason.and_then(|r| match r.as_str() { + "not_interested" => { + Some(crate::services::plugin::recommendations::DismissReason::NotInterested) + } + "already_read" => { + Some(crate::services::plugin::recommendations::DismissReason::AlreadyRead) + } + "already_owned" => { + Some(crate::services::plugin::recommendations::DismissReason::AlreadyOwned) + } + _ => None, + }), + }; + + let response = handle + .call_method::( + methods::RECOMMENDATIONS_DISMISS, + dismiss_request, + ) + .await + .map_err(|e| { + warn!( + plugin_id = %plugin.id, + error = %e, + "Failed to dismiss recommendation" + ); + ApiError::Internal(format!("Recommendation plugin error: {}", e)) + })?; + + Ok(Json(DismissRecommendationResponse { + dismissed: response.dismissed, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::services::plugin::recommendations::Recommendation; + + /// Verify that the Recommendation → RecommendationDto mapping preserves all fields + /// when all optional fields are populated. + #[test] + fn test_to_recommendation_dto_full_fields() { + let rec = Recommendation { + external_id: "12345".to_string(), + external_url: Some("https://anilist.co/manga/12345".to_string()), + title: "Vinland Saga".to_string(), + cover_url: Some("https://img.anilist.co/cover.jpg".to_string()), + summary: Some("A Viking epic about revenge and redemption".to_string()), + genres: vec!["Action".to_string(), "Historical".to_string()], + score: 0.95, + reason: "Because you rated Berserk 10/10".to_string(), + based_on: vec!["Berserk".to_string(), "Vagabond".to_string()], + codex_series_id: Some("codex-uuid-abc".to_string()), + in_library: true, + }; + + let dto = to_recommendation_dto(rec); + + assert_eq!(dto.external_id, "12345"); + assert_eq!( + dto.external_url.as_deref(), + Some("https://anilist.co/manga/12345") + ); + assert_eq!(dto.title, "Vinland Saga"); + assert_eq!( + dto.cover_url.as_deref(), + Some("https://img.anilist.co/cover.jpg") + ); + assert_eq!( + dto.summary.as_deref(), + Some("A Viking epic about revenge and redemption") + ); + assert_eq!(dto.genres, vec!["Action", "Historical"]); + assert!((dto.score - 0.95).abs() < f64::EPSILON); + assert_eq!(dto.reason, "Because you rated Berserk 10/10"); + assert_eq!(dto.based_on, vec!["Berserk", "Vagabond"]); + assert_eq!(dto.codex_series_id.as_deref(), Some("codex-uuid-abc")); + assert!(dto.in_library); + } + + /// Verify that the mapping handles minimal recommendations (None/empty optional fields). + #[test] + fn test_to_recommendation_dto_minimal_fields() { + let rec = Recommendation { + external_id: "99".to_string(), + external_url: None, + title: "Some Manga".to_string(), + cover_url: None, + summary: None, + genres: vec![], + score: 0.5, + reason: "You might like it".to_string(), + based_on: vec![], + codex_series_id: None, + in_library: false, + }; + + let dto = to_recommendation_dto(rec); + + assert_eq!(dto.external_id, "99"); + assert!(dto.external_url.is_none()); + assert_eq!(dto.title, "Some Manga"); + assert!(dto.cover_url.is_none()); + assert!(dto.summary.is_none()); + assert!(dto.genres.is_empty()); + assert!((dto.score - 0.5).abs() < f64::EPSILON); + assert_eq!(dto.reason, "You might like it"); + assert!(dto.based_on.is_empty()); + assert!(dto.codex_series_id.is_none()); + assert!(!dto.in_library); + } + + /// Verify the full RecommendationsResponse can be serialized with the expected JSON shape. + #[test] + fn test_recommendations_response_json_shape() { + let recs = vec![ + to_recommendation_dto(Recommendation { + external_id: "1".to_string(), + external_url: Some("https://example.com/1".to_string()), + title: "Manga A".to_string(), + cover_url: Some("https://img.example.com/a.jpg".to_string()), + summary: Some("Description A".to_string()), + genres: vec!["Action".to_string()], + score: 0.9, + reason: "Based on your library".to_string(), + based_on: vec!["Source A".to_string()], + codex_series_id: None, + in_library: false, + }), + to_recommendation_dto(Recommendation { + external_id: "2".to_string(), + external_url: None, + title: "Manga B".to_string(), + cover_url: None, + summary: None, + genres: vec![], + score: 0.7, + reason: "Popular in your genre".to_string(), + based_on: vec![], + codex_series_id: Some("series-id".to_string()), + in_library: true, + }), + ]; + + let plugin_id = Uuid::new_v4(); + let response = RecommendationsResponse { + recommendations: recs, + plugin_id, + plugin_name: "AniList Recommendations".to_string(), + generated_at: Some("2026-02-09T12:00:00Z".to_string()), + cached: true, + }; + + let json = serde_json::to_value(&response).unwrap(); + + // Top-level fields + assert_eq!(json["pluginId"], plugin_id.to_string()); + assert_eq!(json["pluginName"], "AniList Recommendations"); + assert_eq!(json["generatedAt"], "2026-02-09T12:00:00Z"); + assert!(json["cached"].as_bool().unwrap()); + + // Recommendations array + let recs_arr = json["recommendations"].as_array().unwrap(); + assert_eq!(recs_arr.len(), 2); + + // First recommendation (full fields) + let rec0 = &recs_arr[0]; + assert_eq!(rec0["externalId"], "1"); + assert_eq!(rec0["externalUrl"], "https://example.com/1"); + assert_eq!(rec0["title"], "Manga A"); + assert_eq!(rec0["coverUrl"], "https://img.example.com/a.jpg"); + assert_eq!(rec0["summary"], "Description A"); + assert_eq!(rec0["genres"].as_array().unwrap().len(), 1); + assert_eq!(rec0["score"], 0.9); + assert_eq!(rec0["reason"], "Based on your library"); + assert_eq!(rec0["basedOn"].as_array().unwrap().len(), 1); + assert!(!rec0["inLibrary"].as_bool().unwrap()); + // codexSeriesId should be absent (None) + assert!(rec0.get("codexSeriesId").is_none()); + + // Second recommendation (minimal fields — optional fields absent) + let rec1 = &recs_arr[1]; + assert_eq!(rec1["externalId"], "2"); + assert!(rec1.get("externalUrl").is_none()); + assert_eq!(rec1["title"], "Manga B"); + assert!(rec1.get("coverUrl").is_none()); + assert!(rec1.get("summary").is_none()); + assert!(rec1.get("genres").is_none()); // empty vec is skipped + assert_eq!(rec1["score"], 0.7); + assert!(rec1.get("basedOn").is_none()); // empty vec is skipped + assert_eq!(rec1["codexSeriesId"], "series-id"); + assert!(rec1["inLibrary"].as_bool().unwrap()); + } +} diff --git a/src/api/routes/v1/handlers/user_plugins.rs b/src/api/routes/v1/handlers/user_plugins.rs new file mode 100644 index 00000000..bcc7391b --- /dev/null +++ b/src/api/routes/v1/handlers/user_plugins.rs @@ -0,0 +1,1009 @@ +//! User Plugin Handlers +//! +//! Handlers for user plugin management and OAuth authentication flows. +//! These endpoints allow users to enable/disable plugins, connect via OAuth, +//! and manage their plugin integrations. + +use super::super::dto::plugins::ConfigSchemaDto; +use super::super::dto::user_plugins::{ + AvailablePluginDto, OAuthCallbackQuery, OAuthStartResponse, SetUserCredentialsRequest, + SyncStatusDto, SyncStatusQuery, SyncTriggerResponse, UpdateUserPluginConfigRequest, + UserPluginCapabilitiesDto, UserPluginDto, UserPluginsListResponse, +}; +use crate::api::extractors::auth::AuthContext; +use crate::api::{error::ApiError, extractors::AppState}; +use crate::db::repositories::{ + PluginsRepository, TaskRepository, UserPluginDataRepository, UserPluginsRepository, +}; +use crate::services::plugin::protocol::{OAuthConfig, PluginManifest, methods}; +use crate::services::plugin::sync::SyncStatusResponse; +use crate::tasks::handlers::user_plugin_sync::LAST_SYNC_RESULT_KEY; +use crate::tasks::types::TaskType; +use axum::{ + Json, + extract::{Path, Query, State}, + http::HeaderMap, +}; +use std::sync::Arc; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +/// Parse a plugin's manifest JSON into a typed PluginManifest. +/// Deserializes once and caches the result for callers that need multiple fields. +fn parse_manifest(plugin: &crate::db::entities::plugins::Model) -> Option { + plugin + .manifest + .as_ref() + .and_then(|m| serde_json::from_value(m.clone()).ok()) +} + +/// Helper to extract OAuth config from a plugin's stored manifest +fn get_oauth_config_from_plugin( + plugin: &crate::db::entities::plugins::Model, +) -> Option { + parse_manifest(plugin).and_then(|m| m.oauth) +} + +/// Helper to get the OAuth client_id for a plugin. +/// +/// Priority: plugin config > manifest default +fn get_oauth_client_id(plugin: &crate::db::entities::plugins::Model) -> Option { + // Check plugin config for client_id override + if let Some(client_id) = plugin + .config + .get("oauth_client_id") + .and_then(|v| v.as_str()) + { + return Some(client_id.to_string()); + } + + // Fall back to manifest's default client_id + let oauth_config = get_oauth_config_from_plugin(plugin)?; + oauth_config.client_id +} + +/// Helper to get OAuth client_secret from plugin config +fn get_oauth_client_secret(plugin: &crate::db::entities::plugins::Model) -> Option { + plugin + .config + .get("oauth_client_secret") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) +} + +/// Resolve the external base URL for OAuth redirect URIs. +/// +/// Priority: +/// 1. `redirect_uri_base` from OIDC config (explicit config) +/// 2. `Origin` header from the request (reflects the user's browser URL) +/// 3. Fallback to `http://localhost:3000` +fn resolve_oauth_redirect_base(state: &AppState, headers: &HeaderMap) -> String { + // 1. Explicit config takes priority + if let Some(ref base) = state.auth_config.oidc.redirect_uri_base { + return base.clone(); + } + + // 2. Use Origin header from the request (browser's URL) + if let Some(origin) = headers + .get("origin") + .and_then(|v| v.to_str().ok()) + .filter(|s| !s.is_empty()) + { + return origin.trim_end_matches('/').to_string(); + } + + // 3. Fallback + "http://localhost:3000".to_string() +} + +/// Build a UserPluginDto from a user plugin instance and its parent plugin definition. +/// +/// If `prefetched_sync_result` is `Some`, uses that value directly. +/// If `None`, fetches the last sync result from the database (1 query). +async fn build_user_plugin_dto( + db: &sea_orm::DatabaseConnection, + instance: &crate::db::entities::user_plugins::Model, + plugin: &crate::db::entities::plugins::Model, + prefetched_sync_result: Option>, +) -> UserPluginDto { + let manifest = parse_manifest(plugin); + let oauth_config = manifest.as_ref().and_then(|m| m.oauth.clone()); + + let capabilities = UserPluginCapabilitiesDto { + read_sync: manifest + .as_ref() + .map(|m| m.capabilities.user_read_sync) + .unwrap_or(false), + user_recommendation_provider: manifest + .as_ref() + .map(|m| m.capabilities.user_recommendation_provider) + .unwrap_or(false), + }; + + let user_config_schema = manifest + .as_ref() + .and_then(|m| m.user_config_schema.clone()) + .and_then(|v| serde_json::from_value::(v).ok()); + + // Use pre-fetched value or fetch from DB + let last_sync_result = match prefetched_sync_result { + Some(value) => value, + None => UserPluginDataRepository::get(db, instance.id, LAST_SYNC_RESULT_KEY) + .await + .ok() + .flatten() + .map(|entry| entry.data), + }; + + UserPluginDto { + id: instance.id, + plugin_id: plugin.id, + plugin_name: plugin.name.clone(), + plugin_display_name: plugin.display_name.clone(), + plugin_type: plugin.plugin_type.clone(), + enabled: instance.enabled, + connected: instance.is_authenticated(), + health_status: instance.health_status.clone(), + external_username: instance.external_username.clone(), + external_avatar_url: instance.external_avatar_url.clone(), + last_sync_at: instance.last_sync_at, + last_success_at: instance.last_success_at, + requires_oauth: oauth_config.is_some(), + oauth_configured: get_oauth_client_id(plugin).is_some(), + description: manifest.as_ref().and_then(|m| m.user_description.clone()), + user_setup_instructions: manifest.and_then(|m| m.user_setup_instructions), + config: instance.config.clone(), + capabilities, + user_config_schema, + last_sync_result, + created_at: instance.created_at, + } +} + +/// List user's plugins (enabled and available) +/// +/// Returns both plugins the user has enabled and plugins available for them to enable. +#[utoipa::path( + get, + path = "/api/v1/user/plugins", + responses( + (status = 200, description = "User plugins list", body = UserPluginsListResponse), + (status = 401, description = "Not authenticated"), + ), + tag = "User Plugins" +)] +pub async fn list_user_plugins( + State(state): State>, + auth: AuthContext, +) -> Result, ApiError> { + // Get user's plugin instances + let user_instances = UserPluginsRepository::get_all_for_user(&state.db, auth.user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get user plugins: {}", e)))?; + + // Get all user-type plugins that are enabled by admin + let all_plugins = PluginsRepository::get_all(&state.db) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugins: {}", e)))?; + + let user_plugins: Vec<_> = all_plugins + .iter() + .filter(|p| p.plugin_type == "user" && p.enabled) + .collect(); + + // Batch-fetch all last_sync_result entries (1 query instead of N) + let user_plugin_ids: Vec = user_instances.iter().map(|i| i.id).collect(); + let sync_results_map = UserPluginDataRepository::get_by_key_for_user_plugin_ids( + &state.db, + &user_plugin_ids, + LAST_SYNC_RESULT_KEY, + ) + .await + .unwrap_or_default(); + + // Build enabled plugins list using pre-fetched sync results + let mut enabled: Vec = Vec::new(); + for instance in &user_instances { + if let Some(plugin) = user_plugins.iter().find(|p| p.id == instance.plugin_id) { + let prefetched = sync_results_map + .get(&instance.id) + .map(|entry| entry.data.clone()); + enabled + .push(build_user_plugin_dto(&state.db, instance, plugin, Some(prefetched)).await); + } + } + + // Build available plugins (not yet enabled by user) + let enabled_plugin_ids: std::collections::HashSet<_> = + user_instances.iter().map(|i| i.plugin_id).collect(); + + let available: Vec = user_plugins + .iter() + .filter(|p| !enabled_plugin_ids.contains(&p.id)) + .map(|plugin| { + let manifest = parse_manifest(plugin); + let oauth_config = manifest.as_ref().and_then(|m| m.oauth.clone()); + + AvailablePluginDto { + plugin_id: plugin.id, + name: plugin.name.clone(), + display_name: plugin.display_name.clone(), + description: manifest + .as_ref() + .and_then(|m| m.user_description.clone()) + .or_else(|| manifest.as_ref().and_then(|m| m.description.clone())), + user_setup_instructions: manifest + .as_ref() + .and_then(|m| m.user_setup_instructions.clone()), + requires_oauth: oauth_config.is_some(), + oauth_configured: get_oauth_client_id(plugin).is_some(), + capabilities: UserPluginCapabilitiesDto { + read_sync: manifest + .as_ref() + .map(|m| m.capabilities.user_read_sync) + .unwrap_or(false), + user_recommendation_provider: manifest + .as_ref() + .map(|m| m.capabilities.user_recommendation_provider) + .unwrap_or(false), + }, + } + }) + .collect(); + + Ok(Json(UserPluginsListResponse { enabled, available })) +} + +/// Enable a plugin for the current user +#[utoipa::path( + post, + path = "/api/v1/user/plugins/{plugin_id}/enable", + operation_id = "enable_user_plugin", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to enable") + ), + responses( + (status = 200, description = "Plugin enabled", body = UserPluginDto), + (status = 400, description = "Plugin is not a user plugin or not available"), + (status = 401, description = "Not authenticated"), + (status = 409, description = "Plugin already enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn enable_plugin( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, +) -> Result, ApiError> { + // Verify the plugin exists and is a user plugin + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not found".to_string()))?; + + if plugin.plugin_type != "user" { + return Err(ApiError::BadRequest( + "Only user plugins can be enabled by users".to_string(), + )); + } + + if !plugin.enabled { + return Err(ApiError::BadRequest( + "Plugin is not available (disabled by admin)".to_string(), + )); + } + + // Check if already enabled + if let Some(_existing) = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + { + return Err(ApiError::Conflict( + "Plugin is already enabled for this user".to_string(), + )); + } + + // Create user plugin instance + let instance = UserPluginsRepository::create(&state.db, plugin_id, auth.user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to enable plugin: {}", e)))?; + + info!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + plugin_name = %plugin.name, + "User enabled plugin" + ); + + Ok(Json( + build_user_plugin_dto(&state.db, &instance, &plugin, None).await, + )) +} + +/// Disable a plugin for the current user +#[utoipa::path( + post, + path = "/api/v1/user/plugins/{plugin_id}/disable", + operation_id = "disable_user_plugin", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to disable") + ), + responses( + (status = 200, description = "Plugin disabled"), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn disable_plugin( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, +) -> Result, ApiError> { + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + UserPluginsRepository::set_enabled(&state.db, instance.id, false) + .await + .map_err(|e| ApiError::Internal(format!("Failed to disable plugin: {}", e)))?; + + info!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + "User disabled plugin" + ); + + Ok(Json(serde_json::json!({ "success": true }))) +} + +/// Disconnect a plugin (remove data and credentials) +#[utoipa::path( + delete, + path = "/api/v1/user/plugins/{plugin_id}", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to disconnect") + ), + responses( + (status = 200, description = "Plugin disconnected and data removed"), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn disconnect_plugin( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, +) -> Result, ApiError> { + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + UserPluginsRepository::delete(&state.db, instance.id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to disconnect plugin: {}", e)))?; + + info!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + "User disconnected plugin" + ); + + Ok(Json(serde_json::json!({ "success": true }))) +} + +/// Start OAuth flow for a user plugin +/// +/// Generates an authorization URL and returns it to the client. +/// The client should open this URL in a popup or redirect the user. +#[utoipa::path( + post, + path = "/api/v1/user/plugins/{plugin_id}/oauth/start", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to start OAuth for") + ), + responses( + (status = 200, description = "OAuth authorization URL generated", body = OAuthStartResponse), + (status = 400, description = "Plugin does not support OAuth or not configured"), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not found or not enabled"), + ), + tag = "User Plugins" +)] +pub async fn oauth_start( + State(state): State>, + auth: AuthContext, + headers: HeaderMap, + Path(plugin_id): Path, +) -> Result, ApiError> { + // Get the plugin definition + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not found".to_string()))?; + + if plugin.plugin_type != "user" { + return Err(ApiError::BadRequest( + "Only user plugins support OAuth".to_string(), + )); + } + + // Rate-limit OAuth flow initiation: max 3 pending flows per user + const MAX_PENDING_OAUTH_FLOWS_PER_USER: usize = 3; + let pending = state + .oauth_state_manager + .pending_count_for_user(auth.user_id); + if pending >= MAX_PENDING_OAUTH_FLOWS_PER_USER { + return Err(ApiError::TooManyRequests(format!( + "Too many pending OAuth flows (max {}). Please complete or wait for existing flows to expire.", + MAX_PENDING_OAUTH_FLOWS_PER_USER + ))); + } + + // Get OAuth config from manifest + let oauth_config = get_oauth_config_from_plugin(&plugin).ok_or_else(|| { + ApiError::BadRequest("Plugin does not have OAuth configuration".to_string()) + })?; + + // Get client_id (required) + let client_id = get_oauth_client_id(&plugin).ok_or_else(|| { + ApiError::BadRequest( + "OAuth client_id not configured. Admin must set oauth_client_id in plugin config." + .to_string(), + ) + })?; + + // Ensure user has this plugin enabled (or create the instance) + let _user_plugin = + match UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + { + Some(instance) => instance, + None => { + // Auto-enable the plugin when starting OAuth + UserPluginsRepository::create(&state.db, plugin_id, auth.user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to enable plugin: {}", e)))? + } + }; + + // Build redirect URI using config, request origin, or fallback + let base_url = resolve_oauth_redirect_base(&state, &headers); + let redirect_uri = format!("{}/api/v1/user/plugins/oauth/callback", base_url); + + // Start the OAuth flow + let (auth_url, _state_token) = state + .oauth_state_manager + .start_oauth_flow( + plugin_id, + auth.user_id, + &oauth_config, + &client_id, + &redirect_uri, + ) + .map_err(|e| ApiError::Internal(format!("Failed to start OAuth flow: {}", e)))?; + + debug!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + "Started OAuth flow for user plugin" + ); + + Ok(Json(OAuthStartResponse { + redirect_url: auth_url, + })) +} + +/// Handle OAuth callback from external provider +/// +/// This endpoint receives the callback after the user authenticates with the +/// external service. It exchanges the authorization code for tokens and stores +/// them encrypted in the database. +#[utoipa::path( + get, + path = "/api/v1/user/plugins/oauth/callback", + params( + ("code" = String, Query, description = "Authorization code from OAuth provider"), + ("state" = String, Query, description = "State parameter for CSRF protection"), + ), + responses( + (status = 200, description = "HTML page that auto-closes the popup"), + (status = 400, description = "Invalid callback parameters"), + ), + tag = "User Plugins" +)] +pub async fn oauth_callback( + State(state): State>, + Query(query): Query, +) -> Result, ApiError> { + // Validate state and get pending flow info + let pending = state + .oauth_state_manager + .validate_state(&query.state) + .map_err(|e| { + warn!(error = %e, "OAuth callback state validation failed"); + ApiError::BadRequest(format!("Invalid or expired OAuth state: {}", e)) + })?; + + let plugin_id = pending.plugin_id; + let user_id = pending.user_id; + + debug!( + plugin_id = %plugin_id, + user_id = %user_id, + "Processing OAuth callback" + ); + + // Get plugin and OAuth config + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::Internal("Plugin not found during callback".to_string()))?; + + let oauth_config = get_oauth_config_from_plugin(&plugin).ok_or_else(|| { + ApiError::Internal("Plugin OAuth config missing during callback".to_string()) + })?; + + let client_id = get_oauth_client_id(&plugin) + .ok_or_else(|| ApiError::Internal("OAuth client_id not configured".to_string()))?; + + let client_secret = get_oauth_client_secret(&plugin); + + // Use the redirect_uri from the pending flow to ensure it matches the authorization request + let redirect_uri = pending.redirect_uri.clone(); + + // Exchange code for tokens + let oauth_result = state + .oauth_state_manager + .exchange_code( + &oauth_config, + &query.code, + &client_id, + client_secret.as_deref(), + &redirect_uri, + pending.pkce_verifier.as_deref(), + ) + .await + .map_err(|e| { + warn!(error = %e, plugin_id = %plugin_id, "OAuth code exchange failed"); + ApiError::BadRequest(format!("OAuth authentication failed: {}", e)) + })?; + + // Get or create user plugin instance + let user_plugin = + match UserPluginsRepository::get_by_user_and_plugin(&state.db, user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + { + Some(instance) => instance, + None => UserPluginsRepository::create(&state.db, plugin_id, user_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to create user plugin: {}", e)))?, + }; + + // Store encrypted tokens + UserPluginsRepository::update_oauth_tokens( + &state.db, + user_plugin.id, + &oauth_result.access_token, + oauth_result.refresh_token.as_deref(), + oauth_result.expires_at, + oauth_result.scope.as_deref(), + ) + .await + .map_err(|e| ApiError::Internal(format!("Failed to store OAuth tokens: {}", e)))?; + + // Record success + let _ = UserPluginsRepository::record_success(&state.db, user_plugin.id).await; + + info!( + user_id = %user_id, + plugin_id = %plugin_id, + plugin_name = %plugin.name, + has_refresh_token = oauth_result.refresh_token.is_some(), + "OAuth flow completed successfully" + ); + + // Return a minimal HTML page that closes the popup + let html = r#" +Connected + +
+

Connected successfully!

+

This window will close automatically...

+
+ +"# + .to_string(); + + Ok(axum::response::Html(html)) +} + +/// Get a single user plugin instance +/// +/// Returns detailed status for a plugin the user has enabled. +#[utoipa::path( + get, + path = "/api/v1/user/plugins/{plugin_id}", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID") + ), + responses( + (status = 200, description = "User plugin details", body = UserPluginDto), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn get_user_plugin( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, +) -> Result, ApiError> { + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::Internal("Plugin definition not found".to_string()))?; + + Ok(Json( + build_user_plugin_dto(&state.db, &instance, &plugin, None).await, + )) +} + +/// Update user plugin configuration +/// +/// Allows the user to set per-user configuration overrides for their plugin instance. +#[utoipa::path( + patch, + path = "/api/v1/user/plugins/{plugin_id}/config", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to update config for") + ), + request_body = UpdateUserPluginConfigRequest, + responses( + (status = 200, description = "Configuration updated", body = UserPluginDto), + (status = 400, description = "Invalid configuration"), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn update_user_plugin_config( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, + Json(request): Json, +) -> Result, ApiError> { + // Validate config is a JSON object + if !request.config.is_object() { + return Err(ApiError::BadRequest( + "Config must be a JSON object".to_string(), + )); + } + + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + let updated = UserPluginsRepository::update_config(&state.db, instance.id, request.config) + .await + .map_err(|e| ApiError::Internal(format!("Failed to update config: {}", e)))?; + + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::Internal("Plugin definition not found".to_string()))?; + + debug!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + "User updated plugin config" + ); + + Ok(Json( + build_user_plugin_dto(&state.db, &updated, &plugin, None).await, + )) +} + +/// Trigger a sync operation for a user plugin +/// +/// Enqueues a background sync task that will push/pull reading progress +/// between Codex and the external service. +#[utoipa::path( + post, + path = "/api/v1/user/plugins/{plugin_id}/sync", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to sync") + ), + responses( + (status = 200, description = "Sync task enqueued", body = SyncTriggerResponse), + (status = 400, description = "Plugin is not a sync provider or not connected"), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + (status = 409, description = "Sync already in progress"), + ), + tag = "User Plugins" +)] +pub async fn trigger_sync( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, +) -> Result, ApiError> { + // Verify user has this plugin enabled + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + if !instance.enabled { + return Err(ApiError::BadRequest( + "Plugin is disabled. Enable it before syncing.".to_string(), + )); + } + + // Verify the plugin is a sync provider + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::Internal("Plugin definition not found".to_string()))?; + + let manifest = parse_manifest(&plugin); + let is_read_sync = manifest + .as_ref() + .map(|m| m.capabilities.user_read_sync) + .unwrap_or(false); + + if !is_read_sync { + return Err(ApiError::BadRequest( + "Plugin does not support reading sync".to_string(), + )); + } + + // Verify the plugin is connected (has credentials) + if !instance.is_authenticated() { + return Err(ApiError::BadRequest( + "Plugin is not connected. Complete authentication before syncing.".to_string(), + )); + } + + // Check for duplicate pending/processing sync task + let has_existing = TaskRepository::has_pending_or_processing( + &state.db, + "user_plugin_sync", + plugin_id, + auth.user_id, + ) + .await + .map_err(|e| ApiError::Internal(format!("Failed to check existing tasks: {}", e)))?; + + if has_existing { + return Err(ApiError::Conflict("Sync already in progress".to_string())); + } + + // Enqueue sync task + let task_type = TaskType::UserPluginSync { + plugin_id, + user_id: auth.user_id, + }; + + let task_id = TaskRepository::enqueue(&state.db, task_type, 0, None) + .await + .map_err(|e| ApiError::Internal(format!("Failed to enqueue sync task: {}", e)))?; + + info!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + task_id = %task_id, + plugin_name = %plugin.name, + "Enqueued user plugin sync task" + ); + + Ok(Json(SyncTriggerResponse { + task_id, + message: format!("Sync task enqueued for {}", plugin.display_name), + })) +} + +/// Get sync status for a user plugin +/// +/// Returns the current sync status including last sync time, health, and failure count. +/// Pass `?live=true` to also query the plugin process for live sync state (pending push/pull, +/// conflicts, external entry count). This spawns the plugin process and is more expensive. +#[utoipa::path( + get, + path = "/api/v1/user/plugins/{plugin_id}/sync/status", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to check sync status"), + SyncStatusQuery, + ), + responses( + (status = 200, description = "Sync status", body = SyncStatusDto), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn get_sync_status( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, + Query(query): Query, +) -> Result, ApiError> { + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::Internal("Plugin definition not found".to_string()))?; + + // Optionally query live sync state from the plugin process + let (external_count, pending_push, pending_pull, conflicts, live_error) = if query.live { + debug!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + "Querying live sync status from plugin process" + ); + match state + .plugin_manager + .get_user_plugin_handle(plugin_id, auth.user_id) + .await + { + Ok((handle, _context)) => { + match handle + .call_method::( + methods::SYNC_STATUS, + serde_json::json!({}), + ) + .await + { + Ok(resp) => ( + resp.external_count, + Some(resp.pending_push), + Some(resp.pending_pull), + Some(resp.conflicts), + None, + ), + Err(e) => { + warn!( + plugin_id = %plugin_id, + error = %e, + "Failed to get live sync status from plugin" + ); + ( + None, + None, + None, + None, + Some(format!("sync/status call failed: {}", e)), + ) + } + } + } + Err(e) => { + warn!( + plugin_id = %plugin_id, + error = %e, + "Failed to spawn plugin for live sync status" + ); + ( + None, + None, + None, + None, + Some(format!("Plugin unavailable: {}", e)), + ) + } + } + } else { + (None, None, None, None, None) + }; + + Ok(Json(SyncStatusDto { + plugin_id: plugin.id, + plugin_name: plugin.display_name.clone(), + connected: instance.is_authenticated(), + last_sync_at: instance.last_sync_at, + last_success_at: instance.last_success_at, + last_failure_at: instance.last_failure_at, + health_status: instance.health_status.clone(), + failure_count: instance.failure_count, + enabled: instance.enabled, + external_count, + pending_push, + pending_pull, + conflicts, + live_error, + })) +} + +/// Set user credentials (personal access token) for a plugin +/// +/// Allows users to authenticate by pasting a personal access token +/// instead of going through the OAuth flow. +#[utoipa::path( + post, + path = "/api/v1/user/plugins/{plugin_id}/credentials", + params( + ("plugin_id" = Uuid, Path, description = "Plugin ID to set credentials for") + ), + request_body = SetUserCredentialsRequest, + responses( + (status = 200, description = "Credentials stored", body = UserPluginDto), + (status = 400, description = "Invalid request"), + (status = 401, description = "Not authenticated"), + (status = 404, description = "Plugin not enabled for this user"), + ), + tag = "User Plugins" +)] +pub async fn set_user_credentials( + State(state): State>, + auth: AuthContext, + Path(plugin_id): Path, + Json(request): Json, +) -> Result, ApiError> { + if request.access_token.trim().is_empty() { + return Err(ApiError::BadRequest( + "Access token cannot be empty".to_string(), + )); + } + + let instance = + UserPluginsRepository::get_by_user_and_plugin(&state.db, auth.user_id, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::NotFound("Plugin not enabled for this user".to_string()))?; + + // Store as credentials JSON (same format as required_credentials keys) + let credentials = serde_json::json!({ + "access_token": request.access_token.trim() + }); + + let updated = UserPluginsRepository::update_credentials(&state.db, instance.id, &credentials) + .await + .map_err(|e| ApiError::Internal(format!("Failed to store credentials: {}", e)))?; + + // Record success for health tracking + let _ = UserPluginsRepository::record_success(&state.db, updated.id).await; + + let plugin = PluginsRepository::get_by_id(&state.db, plugin_id) + .await + .map_err(|e| ApiError::Internal(format!("Failed to get plugin: {}", e)))? + .ok_or_else(|| ApiError::Internal("Plugin definition not found".to_string()))?; + + info!( + user_id = %auth.user_id, + plugin_id = %plugin_id, + plugin_name = %plugin.name, + "User set personal access token" + ); + + // Re-fetch to get updated state after record_success + let updated = UserPluginsRepository::get_by_id(&state.db, instance.id) + .await + .map_err(|e| ApiError::Internal(format!("Database error: {}", e)))? + .ok_or_else(|| ApiError::Internal("User plugin not found after update".to_string()))?; + + Ok(Json( + build_user_plugin_dto(&state.db, &updated, &plugin, None).await, + )) +} diff --git a/src/api/routes/v1/routes/mod.rs b/src/api/routes/v1/routes/mod.rs index cf2deac8..e1c278bb 100644 --- a/src/api/routes/v1/routes/mod.rs +++ b/src/api/routes/v1/routes/mod.rs @@ -10,10 +10,12 @@ mod libraries; mod misc; mod oidc; mod plugins; +mod recommendations; mod series; mod setup; mod tasks; mod user; +mod user_plugins; mod users; use crate::api::extractors::AppState; @@ -37,6 +39,8 @@ pub fn create_router(state: Arc) -> Router { .merge(tasks::routes(state.clone())) .merge(misc::routes(state.clone())) .merge(plugins::routes(state.clone())) + .merge(user_plugins::routes(state.clone())) + .merge(recommendations::routes(state.clone())) // Apply state to all routes .with_state(state) } diff --git a/src/api/routes/v1/routes/recommendations.rs b/src/api/routes/v1/routes/recommendations.rs new file mode 100644 index 00000000..702de8b6 --- /dev/null +++ b/src/api/routes/v1/routes/recommendations.rs @@ -0,0 +1,35 @@ +//! Recommendation routes +//! +//! Handles recommendation endpoints: get, refresh, and dismiss. + +use super::super::handlers; +use crate::api::extractors::AppState; +use axum::{ + Router, + routing::{get, post}, +}; +use std::sync::Arc; + +/// Create recommendation routes +/// +/// All routes require authentication. +/// +/// Routes: +/// - GET /user/recommendations - Get personalized recommendations +/// - POST /user/recommendations/refresh - Refresh cached recommendations +/// - POST /user/recommendations/:external_id/dismiss - Dismiss a recommendation +pub fn routes(_state: Arc) -> Router> { + Router::new() + .route( + "/user/recommendations", + get(handlers::recommendations::get_recommendations), + ) + .route( + "/user/recommendations/refresh", + post(handlers::recommendations::refresh_recommendations), + ) + .route( + "/user/recommendations/:external_id/dismiss", + post(handlers::recommendations::dismiss_recommendation), + ) +} diff --git a/src/api/routes/v1/routes/user_plugins.rs b/src/api/routes/v1/routes/user_plugins.rs new file mode 100644 index 00000000..9b9a7245 --- /dev/null +++ b/src/api/routes/v1/routes/user_plugins.rs @@ -0,0 +1,73 @@ +//! User plugin routes +//! +//! Handles user plugin management: listing, enabling/disabling, OAuth flows. + +use super::super::handlers; +use crate::api::extractors::AppState; +use axum::{ + Router, + routing::{get, patch, post}, +}; +use std::sync::Arc; + +/// Create user plugin routes +/// +/// All routes are protected (authentication required) except the OAuth callback. +/// +/// Routes: +/// - List plugins: GET /user/plugins +/// - Get plugin: GET /user/plugins/:plugin_id +/// - Enable: POST /user/plugins/:plugin_id/enable +/// - Disable: POST /user/plugins/:plugin_id/disable +/// - Update config: PATCH /user/plugins/:plugin_id/config +/// - Disconnect: DELETE /user/plugins/:plugin_id +/// - OAuth start: POST /user/plugins/:plugin_id/oauth/start +/// - OAuth callback: GET /user/plugins/oauth/callback (no auth - receives redirect) +pub fn routes(_state: Arc) -> Router> { + Router::new() + // User plugin management + .route( + "/user/plugins", + get(handlers::user_plugins::list_user_plugins), + ) + .route( + "/user/plugins/:plugin_id/enable", + post(handlers::user_plugins::enable_plugin), + ) + .route( + "/user/plugins/:plugin_id/disable", + post(handlers::user_plugins::disable_plugin), + ) + .route( + "/user/plugins/:plugin_id", + get(handlers::user_plugins::get_user_plugin) + .delete(handlers::user_plugins::disconnect_plugin), + ) + .route( + "/user/plugins/:plugin_id/config", + patch(handlers::user_plugins::update_user_plugin_config), + ) + // User credentials (personal access token) + .route( + "/user/plugins/:plugin_id/credentials", + post(handlers::user_plugins::set_user_credentials), + ) + // Sync operations + .route( + "/user/plugins/:plugin_id/sync", + post(handlers::user_plugins::trigger_sync), + ) + .route( + "/user/plugins/:plugin_id/sync/status", + get(handlers::user_plugins::get_sync_status), + ) + // OAuth flow + .route( + "/user/plugins/:plugin_id/oauth/start", + post(handlers::user_plugins::oauth_start), + ) + .route( + "/user/plugins/oauth/callback", + get(handlers::user_plugins::oauth_callback), + ) +} diff --git a/src/commands/common.rs b/src/commands/common.rs index cf5c4ce8..acbf43df 100644 --- a/src/commands/common.rs +++ b/src/commands/common.rs @@ -408,6 +408,7 @@ pub fn spawn_workers( files_config: crate::config::FilesConfig, pdf_page_cache: Option>, plugin_manager: Option>, + oauth_state_manager: Option>, ) -> ( Vec>, Vec>, @@ -446,6 +447,11 @@ pub fn spawn_workers( task_worker = task_worker.with_plugin_manager(pm.clone()); } + // Add OAuth state manager if available (for cleaning up expired OAuth flows) + if let Some(ref osm) = oauth_state_manager { + task_worker = task_worker.with_oauth_state_manager(osm.clone()); + } + let (mut task_worker, worker_shutdown_tx) = task_worker.with_shutdown(); worker_shutdown_channels.push(worker_shutdown_tx); diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 377e63f6..cf90e82e 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -288,6 +288,9 @@ pub async fn serve_command(config_path: PathBuf) -> anyhow::Result<()> { plugin_manager.start_health_checks().await; info!(" Plugin health checks started (60s interval)"); + // Initialize OAuth state manager (shared between API and workers for cleanup) + let oauth_state_manager = Arc::new(crate::services::user_plugin::OAuthStateManager::new()); + // Initialize worker tracking variables let mut worker_handles = Vec::new(); let mut worker_shutdown_channels = Vec::new(); @@ -321,6 +324,7 @@ pub async fn serve_command(config_path: PathBuf) -> anyhow::Result<()> { config.files.clone(), Some(pdf_page_cache.clone()), Some(plugin_manager.clone()), + Some(oauth_state_manager.clone()), ); worker_handles = handles; worker_shutdown_channels = channels; @@ -358,6 +362,7 @@ pub async fn serve_command(config_path: PathBuf) -> anyhow::Result<()> { plugin_manager: plugin_manager.clone(), plugin_metrics_service, oidc_service, + oauth_state_manager: oauth_state_manager.clone(), }); // Build router using API module diff --git a/src/commands/worker.rs b/src/commands/worker.rs index 364c3904..eec9da4a 100644 --- a/src/commands/worker.rs +++ b/src/commands/worker.rs @@ -143,6 +143,7 @@ pub async fn worker_command(config_path: PathBuf) -> anyhow::Result<()> { config.files.clone(), Some(pdf_page_cache), Some(plugin_manager), + None, // No OAuth state manager in standalone worker (no API state to clean) ); info!("All {} task workers started successfully", worker_count); diff --git a/src/db/entities/mod.rs b/src/db/entities/mod.rs index 32fb5ad1..fd7ffbaf 100644 --- a/src/db/entities/mod.rs +++ b/src/db/entities/mod.rs @@ -33,6 +33,10 @@ pub mod users; // OIDC authentication pub mod oidc_connections; +// User plugin system (per-user plugin instances and data storage) +pub mod user_plugin_data; +pub mod user_plugins; + // Series metadata enhancement entities pub mod genres; pub mod series_alternate_titles; diff --git a/src/db/entities/plugins.rs b/src/db/entities/plugins.rs index 2d1c9482..73812a56 100644 --- a/src/db/entities/plugins.rs +++ b/src/db/entities/plugins.rs @@ -139,6 +139,8 @@ pub enum Relation { UpdatedByUser, #[sea_orm(has_many = "super::plugin_failures::Entity")] Failures, + #[sea_orm(has_many = "super::user_plugins::Entity")] + UserPlugins, } impl Related for Entity { @@ -147,6 +149,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserPlugins.def() + } +} + impl ActiveModelBehavior for ActiveModel {} // ============================================================================= @@ -212,9 +220,9 @@ impl std::fmt::Display for PluginHealthStatus { #[serde(rename_all = "snake_case")] pub enum CredentialDelivery { /// Pass credentials as environment variables - #[default] Env, /// Pass credentials in the initialize message + #[default] InitMessage, /// Pass credentials both ways Both, @@ -343,6 +351,9 @@ pub enum PluginPermission { /// Add external links #[serde(rename = "metadata:write:links")] MetadataWriteLinks, + /// Write cross-reference external IDs (e.g., api:anilist, api:myanimelist) + #[serde(rename = "metadata:write:external_ids")] + MetadataWriteExternalIds, /// Update publication year #[serde(rename = "metadata:write:year")] MetadataWriteYear, @@ -433,6 +444,7 @@ impl PluginPermission { PluginPermission::MetadataWriteCovers => "metadata:write:covers", PluginPermission::MetadataWriteRatings => "metadata:write:ratings", PluginPermission::MetadataWriteLinks => "metadata:write:links", + PluginPermission::MetadataWriteExternalIds => "metadata:write:external_ids", PluginPermission::MetadataWriteYear => "metadata:write:year", PluginPermission::MetadataWriteStatus => "metadata:write:status", PluginPermission::MetadataWritePublisher => "metadata:write:publisher", @@ -473,6 +485,7 @@ impl PluginPermission { PluginPermission::MetadataWriteCovers, PluginPermission::MetadataWriteRatings, PluginPermission::MetadataWriteLinks, + PluginPermission::MetadataWriteExternalIds, PluginPermission::MetadataWriteYear, PluginPermission::MetadataWriteStatus, PluginPermission::MetadataWritePublisher, @@ -506,6 +519,7 @@ impl PluginPermission { PluginPermission::MetadataWriteCovers, PluginPermission::MetadataWriteRatings, PluginPermission::MetadataWriteLinks, + PluginPermission::MetadataWriteExternalIds, PluginPermission::MetadataWriteYear, PluginPermission::MetadataWriteStatus, PluginPermission::MetadataWritePublisher, @@ -550,6 +564,7 @@ impl FromStr for PluginPermission { "metadata:write:covers" => Ok(PluginPermission::MetadataWriteCovers), "metadata:write:ratings" => Ok(PluginPermission::MetadataWriteRatings), "metadata:write:links" => Ok(PluginPermission::MetadataWriteLinks), + "metadata:write:external_ids" => Ok(PluginPermission::MetadataWriteExternalIds), "metadata:write:year" => Ok(PluginPermission::MetadataWriteYear), "metadata:write:status" => Ok(PluginPermission::MetadataWriteStatus), "metadata:write:publisher" => Ok(PluginPermission::MetadataWritePublisher), @@ -645,6 +660,7 @@ impl Model { | PluginPermission::MetadataWriteCovers | PluginPermission::MetadataWriteRatings | PluginPermission::MetadataWriteLinks + | PluginPermission::MetadataWriteExternalIds | PluginPermission::MetadataWriteYear | PluginPermission::MetadataWriteStatus | PluginPermission::MetadataWritePublisher @@ -883,8 +899,9 @@ mod tests { // Excluded permissions assert!(!perms.contains(&PluginPermission::MetadataWriteAll)); assert!(!perms.contains(&PluginPermission::MetadataRead)); - // Should have 26 write permissions (14 common + 12 book-specific) - assert_eq!(perms.len(), 26); + assert!(perms.contains(&PluginPermission::MetadataWriteExternalIds)); + // Should have 27 write permissions (15 common + 12 book-specific) + assert_eq!(perms.len(), 27); } #[test] @@ -896,8 +913,9 @@ mod tests { // Book-specific should NOT be in common assert!(!perms.contains(&PluginPermission::MetadataWriteBookType)); assert!(!perms.contains(&PluginPermission::MetadataWriteIsbn)); - // Should have 14 common permissions - assert_eq!(perms.len(), 14); + assert!(perms.contains(&PluginPermission::MetadataWriteExternalIds)); + // Should have 15 common permissions + assert_eq!(perms.len(), 15); } #[test] @@ -966,6 +984,27 @@ mod tests { ); } + #[test] + fn test_external_ids_permission() { + // as_str + assert_eq!( + PluginPermission::MetadataWriteExternalIds.as_str(), + "metadata:write:external_ids" + ); + // from_str + assert_eq!( + PluginPermission::from_str("metadata:write:external_ids").unwrap(), + PluginPermission::MetadataWriteExternalIds + ); + // serialization + let perm = PluginPermission::MetadataWriteExternalIds; + let json = serde_json::to_string(&perm).unwrap(); + assert_eq!(json, "\"metadata:write:external_ids\""); + let deserialized: PluginPermission = + serde_json::from_str("\"metadata:write:external_ids\"").unwrap(); + assert_eq!(deserialized, PluginPermission::MetadataWriteExternalIds); + } + #[test] fn test_book_permission_serialization() { let perm = PluginPermission::MetadataWriteBookType; diff --git a/src/db/entities/prelude.rs b/src/db/entities/prelude.rs index ff4a1465..b0011575 100644 --- a/src/db/entities/prelude.rs +++ b/src/db/entities/prelude.rs @@ -31,6 +31,12 @@ pub use super::plugins::Entity as Plugins; pub use super::series_external_ids::Entity as SeriesExternalIds; pub use super::series_metadata::Entity as SeriesMetadata; +// User plugin system +#[allow(unused_imports)] +pub use super::user_plugin_data::Entity as UserPluginData; +#[allow(unused_imports)] +pub use super::user_plugins::Entity as UserPlugins; + // Sharing tags for content access control (WIP feature) #[allow(unused_imports)] pub use super::series_sharing_tags::Entity as SeriesSharingTags; diff --git a/src/db/entities/user_plugin_data.rs b/src/db/entities/user_plugin_data.rs new file mode 100644 index 00000000..c8ac7431 --- /dev/null +++ b/src/db/entities/user_plugin_data.rs @@ -0,0 +1,123 @@ +//! User Plugin Data entity for per-user plugin key-value storage +//! +//! This entity provides a key-value store scoped per user-plugin instance. +//! Plugins use this to persist stateful data like taste profiles, +//! sync state, cached recommendations, etc. +//! +//! ## Storage Isolation +//! +//! Data isolation is architectural — each entry is scoped to a specific +//! `user_plugin_id`, which itself is scoped to a (plugin_id, user_id) pair. +//! Plugins can only address their own data by key; the host resolves +//! the user+plugin scope from the connection context. +//! +//! ## TTL Support +//! +//! Entries can optionally have an `expires_at` timestamp for cached data. +//! A background cleanup task removes expired entries periodically. + +use chrono::{DateTime, Utc}; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user_plugin_data")] +pub struct Model { + /// Unique identifier for this data entry + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + + /// Reference to the user-plugin instance (provides user + plugin scoping) + pub user_plugin_id: Uuid, + + /// Storage key (e.g., "taste_profile", "recommendations", "sync_state") + pub key: String, + + /// Plugin-managed JSON data + pub data: serde_json::Value, + + /// Optional TTL — entry is considered expired after this timestamp + pub expires_at: Option>, + + /// When this entry was first created + pub created_at: DateTime, + + /// When this entry was last updated + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user_plugins::Entity", + from = "Column::UserPluginId", + to = "super::user_plugins::Column::Id", + on_delete = "Cascade" + )] + UserPlugin, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserPlugin.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +// ============================================================================= +// Helper Methods +// ============================================================================= + +impl Model { + /// Check if this entry has expired + pub fn is_expired(&self) -> bool { + match self.expires_at { + Some(expires_at) => Utc::now() >= expires_at, + None => false, // No expiry means never expires + } + } +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Duration; + + fn test_model() -> Model { + Model { + id: Uuid::new_v4(), + user_plugin_id: Uuid::new_v4(), + key: "test_key".to_string(), + data: serde_json::json!({"value": 42}), + expires_at: None, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + + #[test] + fn test_is_expired_no_expiry() { + let model = test_model(); + assert!(!model.is_expired()); + } + + #[test] + fn test_is_expired_future() { + let mut model = test_model(); + model.expires_at = Some(Utc::now() + Duration::hours(1)); + assert!(!model.is_expired()); + } + + #[test] + fn test_is_expired_past() { + let mut model = test_model(); + model.expires_at = Some(Utc::now() - Duration::hours(1)); + assert!(model.is_expired()); + } +} diff --git a/src/db/entities/user_plugins.rs b/src/db/entities/user_plugins.rs new file mode 100644 index 00000000..6f35da0c --- /dev/null +++ b/src/db/entities/user_plugins.rs @@ -0,0 +1,325 @@ +//! User Plugin entity for per-user plugin instances +//! +//! This entity links users to plugins they've enabled, storing per-user +//! credentials (encrypted OAuth tokens, API keys), configuration overrides, +//! and external identity information. +//! +//! ## Key Features +//! +//! - **Per-user credentials**: Encrypted OAuth tokens or API keys per user +//! - **OAuth integration**: Access/refresh tokens, expiry tracking, external identity +//! - **Health tracking**: Per-user failure count and health status +//! - **Sync state**: Last sync timestamp for sync provider plugins +//! +//! ## Lifecycle +//! +//! 1. Admin installs a user-type plugin (in `plugins` table) +//! 2. User enables the plugin (creates `user_plugins` row) +//! 3. User connects via OAuth or provides API key +//! 4. User can disable/disconnect independently of other users + +#![allow(dead_code)] + +use chrono::{DateTime, Duration, Utc}; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::plugins::PluginHealthStatus; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user_plugins")] +pub struct Model { + /// Unique identifier for this user-plugin instance + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + + /// Reference to the plugin definition + pub plugin_id: Uuid, + + /// The user who enabled this plugin + pub user_id: Uuid, + + /// Encrypted per-user credentials (simple API keys/tokens) + #[serde(skip_serializing)] + pub credentials: Option>, + + /// Per-user configuration overrides (merged with plugin defaults) + pub config: serde_json::Value, + + /// Encrypted OAuth access token + #[serde(skip_serializing)] + pub oauth_access_token: Option>, + + /// Encrypted OAuth refresh token + #[serde(skip_serializing)] + pub oauth_refresh_token: Option>, + + /// When the OAuth access token expires + pub oauth_expires_at: Option>, + + /// OAuth scopes granted by the user + pub oauth_scope: Option, + + /// External user ID from the connected service (e.g., AniList user ID) + pub external_user_id: Option, + + /// External username for display (e.g., "@username" on AniList) + pub external_username: Option, + + /// External avatar URL for display + pub external_avatar_url: Option, + + /// Whether this user-plugin instance is enabled + pub enabled: bool, + + /// Current health status: "unknown", "healthy", "degraded", "unhealthy", "disabled" + pub health_status: String, + + /// Number of consecutive failures + pub failure_count: i32, + + /// When the last failure occurred + pub last_failure_at: Option>, + + /// When the last successful operation occurred + pub last_success_at: Option>, + + /// When the last sync operation completed + pub last_sync_at: Option>, + + /// When this user-plugin instance was created + pub created_at: DateTime, + + /// When this user-plugin instance was last updated + pub updated_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::plugins::Entity", + from = "Column::PluginId", + to = "super::plugins::Column::Id", + on_delete = "Cascade" + )] + Plugin, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_delete = "Cascade" + )] + User, + #[sea_orm(has_many = "super::user_plugin_data::Entity")] + UserPluginData, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Plugin.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserPluginData.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +// ============================================================================= +// Helper Methods +// ============================================================================= + +impl Model { + /// Check if the OAuth access token has expired + pub fn is_oauth_expired(&self) -> bool { + match self.oauth_expires_at { + Some(expires_at) => Utc::now() >= expires_at, + None => false, // No expiry means no OAuth or non-expiring token + } + } + + /// Check if the OAuth token needs refreshing (within 5 minutes of expiry) + pub fn needs_token_refresh(&self) -> bool { + match self.oauth_expires_at { + Some(expires_at) => { + let refresh_buffer = Duration::minutes(5); + Utc::now() >= (expires_at - refresh_buffer) + } + None => false, + } + } + + /// Check if this instance has OAuth tokens configured + pub fn has_oauth_tokens(&self) -> bool { + self.oauth_access_token.is_some() + } + + /// Check if this instance has simple credentials configured + pub fn has_credentials(&self) -> bool { + self.credentials.is_some() + } + + /// Check if the instance has any form of authentication configured + pub fn is_authenticated(&self) -> bool { + self.has_oauth_tokens() || self.has_credentials() + } + + /// Parse health status + pub fn health_status_type(&self) -> PluginHealthStatus { + self.health_status + .parse() + .unwrap_or(PluginHealthStatus::Unknown) + } + + /// Check if the instance is in a healthy state + pub fn is_healthy(&self) -> bool { + self.enabled + && matches!( + self.health_status_type(), + PluginHealthStatus::Healthy | PluginHealthStatus::Unknown + ) + } +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + fn test_model() -> Model { + Model { + id: Uuid::new_v4(), + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + credentials: None, + config: serde_json::json!({}), + oauth_access_token: None, + oauth_refresh_token: None, + oauth_expires_at: None, + oauth_scope: None, + external_user_id: None, + external_username: None, + external_avatar_url: None, + enabled: true, + health_status: "unknown".to_string(), + failure_count: 0, + last_failure_at: None, + last_success_at: None, + last_sync_at: None, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + + #[test] + fn test_is_oauth_expired_no_expiry() { + let model = test_model(); + assert!(!model.is_oauth_expired()); + } + + #[test] + fn test_is_oauth_expired_future() { + let mut model = test_model(); + model.oauth_expires_at = Some(Utc::now() + Duration::hours(1)); + assert!(!model.is_oauth_expired()); + } + + #[test] + fn test_is_oauth_expired_past() { + let mut model = test_model(); + model.oauth_expires_at = Some(Utc::now() - Duration::hours(1)); + assert!(model.is_oauth_expired()); + } + + #[test] + fn test_needs_token_refresh_no_expiry() { + let model = test_model(); + assert!(!model.needs_token_refresh()); + } + + #[test] + fn test_needs_token_refresh_far_future() { + let mut model = test_model(); + model.oauth_expires_at = Some(Utc::now() + Duration::hours(1)); + assert!(!model.needs_token_refresh()); + } + + #[test] + fn test_needs_token_refresh_within_buffer() { + let mut model = test_model(); + model.oauth_expires_at = Some(Utc::now() + Duration::minutes(3)); + assert!(model.needs_token_refresh()); + } + + #[test] + fn test_has_oauth_tokens() { + let mut model = test_model(); + assert!(!model.has_oauth_tokens()); + + model.oauth_access_token = Some(vec![1, 2, 3]); + assert!(model.has_oauth_tokens()); + } + + #[test] + fn test_has_credentials() { + let mut model = test_model(); + assert!(!model.has_credentials()); + + model.credentials = Some(vec![1, 2, 3]); + assert!(model.has_credentials()); + } + + #[test] + fn test_is_authenticated() { + let mut model = test_model(); + assert!(!model.is_authenticated()); + + model.credentials = Some(vec![1, 2, 3]); + assert!(model.is_authenticated()); + + model.credentials = None; + model.oauth_access_token = Some(vec![4, 5, 6]); + assert!(model.is_authenticated()); + } + + #[test] + fn test_health_status_type() { + let mut model = test_model(); + assert_eq!(model.health_status_type(), PluginHealthStatus::Unknown); + + model.health_status = "healthy".to_string(); + assert_eq!(model.health_status_type(), PluginHealthStatus::Healthy); + + model.health_status = "unhealthy".to_string(); + assert_eq!(model.health_status_type(), PluginHealthStatus::Unhealthy); + } + + #[test] + fn test_is_healthy() { + let mut model = test_model(); + assert!(model.is_healthy()); // enabled + unknown = healthy + + model.health_status = "healthy".to_string(); + assert!(model.is_healthy()); + + model.health_status = "unhealthy".to_string(); + assert!(!model.is_healthy()); + + model.health_status = "healthy".to_string(); + model.enabled = false; + assert!(!model.is_healthy()); + } +} diff --git a/src/db/entities/users.rs b/src/db/entities/users.rs index 7d491049..db101116 100644 --- a/src/db/entities/users.rs +++ b/src/db/entities/users.rs @@ -41,6 +41,8 @@ pub enum Relation { UserSharingTags, #[sea_orm(has_many = "super::oidc_connections::Entity")] OidcConnections, + #[sea_orm(has_many = "super::user_plugins::Entity")] + UserPlugins, } impl Related for Entity { @@ -70,4 +72,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserPlugins.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/src/db/repositories/book.rs b/src/db/repositories/book.rs index 35c72d5a..c5e83ac7 100644 --- a/src/db/repositories/book.rs +++ b/src/db/repositories/book.rs @@ -552,6 +552,34 @@ impl BookRepository { .context("Failed to list books by series IDs") } + /// Get all books in multiple series, grouped by series ID + /// + /// Returns a HashMap keyed by series_id for efficient lookups. + /// Each value is a Vec of non-deleted books belonging to that series. + pub async fn get_by_series_ids( + db: &DatabaseConnection, + series_ids: &[Uuid], + ) -> Result>> { + if series_ids.is_empty() { + return Ok(std::collections::HashMap::new()); + } + + let results = Books::find() + .filter(books::Column::SeriesId.is_in(series_ids.to_vec())) + .filter(books::Column::Deleted.eq(false)) + .all(db) + .await + .context("Failed to list books by series IDs")?; + + let mut map: std::collections::HashMap> = + std::collections::HashMap::new(); + for book in results { + map.entry(book.series_id).or_default().push(book); + } + + Ok(map) + } + /// Count books in a series (excluding deleted) pub async fn count_by_series(db: &DatabaseConnection, series_id: Uuid) -> Result { Books::find() @@ -3800,4 +3828,103 @@ mod tests { assert_eq!(counts.get(&BookErrorType::Thumbnail), Some(&2)); assert_eq!(counts.get(&BookErrorType::Metadata), None); } + + #[tokio::test] + async fn test_get_by_series_ids_empty_input() { + let (db, _temp_dir) = create_test_db().await; + + let result = BookRepository::get_by_series_ids(db.sea_orm_connection(), &[]) + .await + .unwrap(); + + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_get_by_series_ids_multiple_series() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series1 = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Series 1", None) + .await + .unwrap(); + let series2 = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Series 2", None) + .await + .unwrap(); + + let book1 = create_book_model(series1.id, library.id, "/test/book1.cbz", "book1.cbz"); + let book2 = create_book_model(series1.id, library.id, "/test/book2.cbz", "book2.cbz"); + let book3 = create_book_model(series2.id, library.id, "/test/book3.cbz", "book3.cbz"); + let book4 = create_book_model(series2.id, library.id, "/test/book4.cbz", "book4.cbz"); + + BookRepository::create(db.sea_orm_connection(), &book1, None) + .await + .unwrap(); + BookRepository::create(db.sea_orm_connection(), &book2, None) + .await + .unwrap(); + BookRepository::create(db.sea_orm_connection(), &book3, None) + .await + .unwrap(); + BookRepository::create(db.sea_orm_connection(), &book4, None) + .await + .unwrap(); + + let result = + BookRepository::get_by_series_ids(db.sea_orm_connection(), &[series1.id, series2.id]) + .await + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result.get(&series1.id).unwrap().len(), 2); + assert_eq!(result.get(&series2.id).unwrap().len(), 2); + } + + #[tokio::test] + async fn test_get_by_series_ids_excludes_deleted_books() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Test Series", None) + .await + .unwrap(); + + let book1 = create_book_model(series.id, library.id, "/test/book1.cbz", "book1.cbz"); + let mut book2 = create_book_model(series.id, library.id, "/test/book2.cbz", "book2.cbz"); + book2.deleted = true; + + BookRepository::create(db.sea_orm_connection(), &book1, None) + .await + .unwrap(); + BookRepository::create(db.sea_orm_connection(), &book2, None) + .await + .unwrap(); + + let result = BookRepository::get_by_series_ids(db.sea_orm_connection(), &[series.id]) + .await + .unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result.get(&series.id).unwrap().len(), 1); + assert_eq!(result.get(&series.id).unwrap()[0].id, book1.id); + } } diff --git a/src/db/repositories/mod.rs b/src/db/repositories/mod.rs index 71e0b101..491f45eb 100644 --- a/src/db/repositories/mod.rs +++ b/src/db/repositories/mod.rs @@ -34,6 +34,10 @@ pub mod sharing_tag; // OIDC authentication pub mod oidc_connection; +// User plugin system +pub mod user_plugin_data; +pub mod user_plugins; + // Re-export repositories pub use alternate_title::AlternateTitleRepository; pub use api_key::ApiKeyRepository; @@ -72,3 +76,8 @@ pub use sharing_tag::SharingTagRepository; // OIDC authentication pub use oidc_connection::OidcConnectionRepository; + +// User plugin system +#[allow(unused_imports)] +pub use user_plugin_data::UserPluginDataRepository; +pub use user_plugins::UserPluginsRepository; diff --git a/src/db/repositories/plugins.rs b/src/db/repositories/plugins.rs index 33be29d1..27454f49 100644 --- a/src/db/repositories/plugins.rs +++ b/src/db/repositories/plugins.rs @@ -16,7 +16,7 @@ use crate::db::entities::plugins::{self, Entity as Plugins, PluginPermission}; use crate::services::CredentialEncryption; -use crate::services::plugin::protocol::PluginScope; +use crate::services::plugin::protocol::{PluginManifest, PluginScope}; use anyhow::{Result, anyhow}; use chrono::Utc; use sea_orm::*; @@ -467,6 +467,14 @@ impl PluginsRepository { } /// Update cached manifest from plugin + /// + /// Also infers and syncs the `plugin_type` column from the manifest + /// capabilities (e.g. `userReadSync` or `userRecommendationProvider` → "user"). + /// + /// If the manifest declares `requiredCredentials` or `oauth`, the + /// `credential_delivery` column is set to `"init_message"` so that + /// credentials are passed in the JSON-RPC initialize message (which is + /// what SDK-based plugins expect). pub async fn update_manifest( db: &DatabaseConnection, id: Uuid, @@ -477,6 +485,22 @@ impl PluginsRepository { .ok_or_else(|| anyhow!("Plugin not found: {}", id))?; let mut active_model: plugins::ActiveModel = existing.into(); + + if let Some(ref manifest_json) = manifest + && let Ok(parsed) = serde_json::from_value::(manifest_json.clone()) + { + // Infer plugin_type from manifest capabilities + if let Some(inferred) = parsed.capabilities.inferred_plugin_type() { + active_model.plugin_type = Set(inferred.to_string()); + } + + // If the plugin requires credentials or OAuth, ensure they are + // delivered via init_message (SDK plugins read from params.credentials) + if !parsed.required_credentials.is_empty() || parsed.oauth.is_some() { + active_model.credential_delivery = Set("init_message".to_string()); + } + } + active_model.manifest = Set(manifest); active_model.updated_at = Set(Utc::now()); @@ -1159,6 +1183,105 @@ mod tests { assert!(updated.manifest.is_some()); assert_eq!(updated.manifest.unwrap()["version"], "1.0.0"); + // No user capabilities → plugin_type stays "system" + assert_eq!(updated.plugin_type, "system"); + } + + #[tokio::test] + async fn test_update_manifest_infers_user_plugin_type() { + setup_test_encryption_key(); + let db = setup_test_db().await; + + let plugin = PluginsRepository::create( + &db, + "sync-test", + "Sync Test", + None, + "system", // starts as system + "node", + vec![], + vec![], + None, + vec![], + vec![], + vec![], + None, + "env", + None, + false, + None, + Some(60), + ) + .await + .unwrap(); + + assert_eq!(plugin.plugin_type, "system"); + + // Manifest with userReadSync capability should flip plugin_type to "user" + let manifest = serde_json::json!({ + "name": "sync-test", + "displayName": "Sync Test", + "version": "1.0.0", + "protocolVersion": "1.0", + "capabilities": { + "userReadSync": true + } + }); + + let updated = PluginsRepository::update_manifest(&db, plugin.id, Some(manifest)) + .await + .unwrap(); + + assert_eq!(updated.plugin_type, "user"); + } + + #[tokio::test] + async fn test_update_manifest_infers_system_plugin_type() { + setup_test_encryption_key(); + let db = setup_test_db().await; + + // Start as "user" to verify it gets corrected + let plugin = PluginsRepository::create( + &db, + "metadata-test", + "Metadata Test", + None, + "user", + "node", + vec![], + vec![], + None, + vec![], + vec![], + vec![], + None, + "env", + None, + false, + None, + Some(60), + ) + .await + .unwrap(); + + assert_eq!(plugin.plugin_type, "user"); + + // Manifest with metadataProvider capability should set plugin_type to "system" + let manifest = serde_json::json!({ + "name": "metadata-test", + "displayName": "Metadata Test", + "version": "1.0.0", + "protocolVersion": "1.0", + "capabilities": { + "metadataProvider": ["series"] + } + }); + + let updated = PluginsRepository::update_manifest(&db, plugin.id, Some(manifest)) + .await + .unwrap(); + + assert_eq!(updated.plugin_type, "system"); } #[tokio::test] diff --git a/src/db/repositories/read_progress.rs b/src/db/repositories/read_progress.rs index 1ab53526..b92d76f4 100644 --- a/src/db/repositories/read_progress.rs +++ b/src/db/repositories/read_progress.rs @@ -223,6 +223,28 @@ impl ReadProgressRepository { Self::delete(db, user_id, book_id).await } + /// Get reading progress for a user across multiple books + /// + /// Returns a HashMap keyed by book_id for efficient lookups. + /// Only returns books that have progress records for the given user. + pub async fn get_for_user_books( + db: &DatabaseConnection, + user_id: Uuid, + book_ids: &[Uuid], + ) -> Result> { + if book_ids.is_empty() { + return Ok(std::collections::HashMap::new()); + } + + let results = ReadProgress::find() + .filter(read_progress::Column::UserId.eq(user_id)) + .filter(read_progress::Column::BookId.is_in(book_ids.to_vec())) + .all(db) + .await?; + + Ok(results.into_iter().map(|p| (p.book_id, p)).collect()) + } + /// Mark all books in a series as read for a user /// Returns the number of books marked as read pub async fn mark_series_as_read( @@ -635,4 +657,50 @@ mod tests { .unwrap(); assert_eq!(all_progress.len(), 1); } + + #[tokio::test] + async fn test_get_for_user_books_empty_input() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + + let result = ReadProgressRepository::get_for_user_books(&db, user.id, &[]) + .await + .unwrap(); + + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_get_for_user_books_multiple_books() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let book1 = create_test_book(&db).await; + let book2 = create_test_book(&db).await; + let book3 = create_test_book(&db).await; + + // Create progress for book1 and book2 only + ReadProgressRepository::upsert(&db, user.id, book1.id, 10, false) + .await + .unwrap(); + ReadProgressRepository::upsert(&db, user.id, book2.id, 25, true) + .await + .unwrap(); + + // Query for all three books — only two should have progress + let result = ReadProgressRepository::get_for_user_books( + &db, + user.id, + &[book1.id, book2.id, book3.id], + ) + .await + .unwrap(); + + assert_eq!(result.len(), 2); + assert!(result.contains_key(&book1.id)); + assert!(result.contains_key(&book2.id)); + assert!(!result.contains_key(&book3.id)); + assert_eq!(result.get(&book1.id).unwrap().current_page, 10); + assert_eq!(result.get(&book2.id).unwrap().current_page, 25); + assert!(result.get(&book2.id).unwrap().completed); + } } diff --git a/src/db/repositories/series_external_id.rs b/src/db/repositories/series_external_id.rs index 25adaae5..984dffb2 100644 --- a/src/db/repositories/series_external_id.rs +++ b/src/db/repositories/series_external_id.rs @@ -250,6 +250,31 @@ impl SeriesExternalIdRepository { Ok(map) } + /// Find series external IDs by multiple external ID values and source + /// + /// Returns a HashMap keyed by external_id for efficient reverse lookups. + /// Used during pull sync to batch-match pulled entries to Codex series. + pub async fn find_by_external_ids_and_source( + db: &DatabaseConnection, + external_ids: &[String], + source: &str, + ) -> Result> { + if external_ids.is_empty() { + return Ok(HashMap::new()); + } + + let results = SeriesExternalIds::find() + .filter(series_external_ids::Column::ExternalId.is_in(external_ids.to_vec())) + .filter(series_external_ids::Column::Source.eq(source)) + .all(db) + .await?; + + Ok(results + .into_iter() + .map(|e| (e.external_id.clone(), e)) + .collect()) + } + /// Check if an external ID record belongs to a specific series pub async fn belongs_to_series( db: &DatabaseConnection, @@ -283,6 +308,23 @@ impl SeriesExternalIdRepository { .await?; Ok(results) } + + /// Find a series external ID by external ID value and source + /// + /// Used for reverse lookups, e.g. finding which Codex series has a given + /// AniList media ID (`source = "api:anilist"`, `external_id = "12345"`). + pub async fn find_by_external_id_and_source( + db: &DatabaseConnection, + external_id: &str, + source: &str, + ) -> Result> { + let result = SeriesExternalIds::find() + .filter(series_external_ids::Column::ExternalId.eq(external_id)) + .filter(series_external_ids::Column::Source.eq(source)) + .one(db) + .await?; + Ok(result) + } } #[cfg(test)] @@ -1008,4 +1050,275 @@ mod tests { assert_eq!(comicinfo_ids.len(), 1); } + + #[tokio::test] + async fn test_find_by_external_id_and_source() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series1 = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Series 1", None) + .await + .unwrap(); + + let series2 = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Series 2", None) + .await + .unwrap(); + + // Create api:anilist external IDs for both series + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series1.id, + "api:anilist", + "12345", + None, + None, + ) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series2.id, + "api:anilist", + "67890", + None, + None, + ) + .await + .unwrap(); + + // Also create a different source with the same external ID + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series1.id, + "api:myanimelist", + "12345", + None, + None, + ) + .await + .unwrap(); + + // Find by anilist ID + let found = SeriesExternalIdRepository::find_by_external_id_and_source( + db.sea_orm_connection(), + "12345", + "api:anilist", + ) + .await + .unwrap(); + assert!(found.is_some()); + assert_eq!(found.unwrap().series_id, series1.id); + + // Find different anilist ID + let found = SeriesExternalIdRepository::find_by_external_id_and_source( + db.sea_orm_connection(), + "67890", + "api:anilist", + ) + .await + .unwrap(); + assert!(found.is_some()); + assert_eq!(found.unwrap().series_id, series2.id); + + // Non-existent external ID returns None + let not_found = SeriesExternalIdRepository::find_by_external_id_and_source( + db.sea_orm_connection(), + "99999", + "api:anilist", + ) + .await + .unwrap(); + assert!(not_found.is_none()); + + // Same external ID but different source returns correct result + let found_mal = SeriesExternalIdRepository::find_by_external_id_and_source( + db.sea_orm_connection(), + "12345", + "api:myanimelist", + ) + .await + .unwrap(); + assert!(found_mal.is_some()); + assert_eq!(found_mal.unwrap().series_id, series1.id); + } + + #[tokio::test] + async fn test_find_by_external_ids_and_source_empty_input() { + let (db, _temp_dir) = create_test_db().await; + + let result = SeriesExternalIdRepository::find_by_external_ids_and_source( + db.sea_orm_connection(), + &[], + "plugin:mangabaka", + ) + .await + .unwrap(); + + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_find_by_external_ids_and_source_multiple_ids() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series1 = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Series 1", None) + .await + .unwrap(); + let series2 = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Series 2", None) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series1.id, + "api:anilist", + "ext_1", + None, + None, + ) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series2.id, + "api:anilist", + "ext_2", + None, + None, + ) + .await + .unwrap(); + + let result = SeriesExternalIdRepository::find_by_external_ids_and_source( + db.sea_orm_connection(), + &["ext_1".to_string(), "ext_2".to_string()], + "api:anilist", + ) + .await + .unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result.get("ext_1").unwrap().series_id, series1.id); + assert_eq!(result.get("ext_2").unwrap().series_id, series2.id); + } + + #[tokio::test] + async fn test_find_by_external_ids_and_source_filters_by_source() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Test Series", None) + .await + .unwrap(); + + // Same external_id but different sources + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "12345", + None, + None, + ) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:myanimelist", + "12345", + None, + None, + ) + .await + .unwrap(); + + let result = SeriesExternalIdRepository::find_by_external_ids_and_source( + db.sea_orm_connection(), + &["12345".to_string()], + "api:anilist", + ) + .await + .unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result.get("12345").unwrap().source, "api:anilist"); + } + + #[tokio::test] + async fn test_find_by_external_ids_and_source_partial_match() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Test Series", None) + .await + .unwrap(); + + // Only one external ID exists + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "111", + None, + None, + ) + .await + .unwrap(); + + // Query for multiple IDs — only one should match + let result = SeriesExternalIdRepository::find_by_external_ids_and_source( + db.sea_orm_connection(), + &["111".to_string(), "222".to_string(), "333".to_string()], + "api:anilist", + ) + .await + .unwrap(); + + assert_eq!(result.len(), 1); + assert!(result.contains_key("111")); + assert!(!result.contains_key("222")); + } } diff --git a/src/db/repositories/task.rs b/src/db/repositories/task.rs index d58def6a..523fe857 100644 --- a/src/db/repositories/task.rs +++ b/src/db/repositories/task.rs @@ -1,8 +1,8 @@ use anyhow::{Context, Result}; use chrono::{DateTime, Duration, Utc}; use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, DbBackend, EntityTrait, QueryFilter, - QueryOrder, QuerySelect, Set, Statement, TransactionTrait, entity::prelude::*, + ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseConnection, DbBackend, EntityTrait, + QueryFilter, QueryOrder, QuerySelect, Set, Statement, TransactionTrait, entity::prelude::*, }; use tracing::{info, warn}; use uuid::Uuid; @@ -272,6 +272,52 @@ impl TaskRepository { query.one(db).await.context("Failed to find existing task") } + /// Check if a pending or processing task exists with matching params. + /// + /// Used for task types that store their identity in JSON params rather than + /// FK columns (e.g., UserPluginSync, UserPluginRecommendations). Uses + /// database-level JSON filtering to avoid loading all tasks into memory. + pub async fn has_pending_or_processing( + db: &DatabaseConnection, + task_type: &str, + plugin_id: Uuid, + user_id: Uuid, + ) -> Result { + let plugin_id_str = plugin_id.to_string(); + let user_id_str = user_id.to_string(); + let backend = db.get_database_backend(); + + let stmt = match backend { + DbBackend::Postgres => Statement::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT 1 FROM tasks + WHERE task_type = $1 + AND status IN ('pending', 'processing') + AND params->>'plugin_id' = $2 + AND params->>'user_id' = $3 + LIMIT 1"#, + vec![task_type.into(), plugin_id_str.into(), user_id_str.into()], + ), + _ => Statement::from_sql_and_values( + DbBackend::Sqlite, + r#"SELECT 1 FROM tasks + WHERE task_type = ? + AND status IN ('pending', 'processing') + AND json_extract(params, '$.plugin_id') = ? + AND json_extract(params, '$.user_id') = ? + LIMIT 1"#, + vec![task_type.into(), plugin_id_str.into(), user_id_str.into()], + ), + }; + + let result = db + .query_one(stmt) + .await + .context("Failed to check for existing tasks")?; + + Ok(result.is_some()) + } + /// Claim next available task (atomic operation using SKIP LOCKED for Postgres, transaction for SQLite) /// /// # Arguments diff --git a/src/db/repositories/user_plugin_data.rs b/src/db/repositories/user_plugin_data.rs new file mode 100644 index 00000000..e1f464a5 --- /dev/null +++ b/src/db/repositories/user_plugin_data.rs @@ -0,0 +1,659 @@ +//! User Plugin Data Repository +//! +//! Provides key-value storage operations for per-user plugin data. +//! Plugins use this to persist stateful data like taste profiles, +//! sync state, and cached recommendations. +//! +//! ## Key Features +//! +//! - Get/set/delete key-value pairs scoped per user-plugin instance +//! - Optional TTL (time-to-live) for cached data +//! - List all keys for a plugin instance +//! - Clear all data for a plugin instance +//! - Background cleanup of expired data + +#![allow(dead_code)] + +use crate::db::entities::user_plugin_data::{self, Entity as UserPluginData}; +use anyhow::Result; +use chrono::{DateTime, Utc}; +use sea_orm::*; +use std::collections::HashMap; +use uuid::Uuid; + +pub struct UserPluginDataRepository; + +impl UserPluginDataRepository { + // ========================================================================= + // Read Operations + // ========================================================================= + + /// Get a value by key for a user plugin instance + /// + /// Returns None if the key doesn't exist or if the entry has expired. + pub async fn get( + db: &DatabaseConnection, + user_plugin_id: Uuid, + key: &str, + ) -> Result> { + let entry = UserPluginData::find() + .filter(user_plugin_data::Column::UserPluginId.eq(user_plugin_id)) + .filter(user_plugin_data::Column::Key.eq(key)) + .one(db) + .await?; + + // Check if expired + match entry { + Some(e) if e.is_expired() => { + // Auto-delete expired entry + UserPluginData::delete_by_id(e.id).exec(db).await?; + Ok(None) + } + other => Ok(other), + } + } + + /// List all keys for a user plugin instance (excluding expired) + pub async fn list_keys( + db: &DatabaseConnection, + user_plugin_id: Uuid, + ) -> Result> { + let entries = UserPluginData::find() + .filter(user_plugin_data::Column::UserPluginId.eq(user_plugin_id)) + .filter( + Condition::any() + .add(user_plugin_data::Column::ExpiresAt.is_null()) + .add(user_plugin_data::Column::ExpiresAt.gt(Utc::now())), + ) + .order_by_asc(user_plugin_data::Column::Key) + .all(db) + .await?; + Ok(entries) + } + + /// Get values by key for multiple user plugin instances + /// + /// Returns a HashMap keyed by user_plugin_id for efficient lookups. + /// Excludes expired entries (but does NOT auto-delete them — use + /// `cleanup_expired` for that). + pub async fn get_by_key_for_user_plugin_ids( + db: &DatabaseConnection, + user_plugin_ids: &[Uuid], + key: &str, + ) -> Result> { + if user_plugin_ids.is_empty() { + return Ok(HashMap::new()); + } + + let results = UserPluginData::find() + .filter(user_plugin_data::Column::UserPluginId.is_in(user_plugin_ids.to_vec())) + .filter(user_plugin_data::Column::Key.eq(key)) + .filter( + Condition::any() + .add(user_plugin_data::Column::ExpiresAt.is_null()) + .add(user_plugin_data::Column::ExpiresAt.gt(Utc::now())), + ) + .all(db) + .await?; + + Ok(results.into_iter().map(|e| (e.user_plugin_id, e)).collect()) + } + + // ========================================================================= + // Write Operations + // ========================================================================= + + /// Set a value by key (upsert - creates or updates) + pub async fn set( + db: &DatabaseConnection, + user_plugin_id: Uuid, + key: &str, + data: serde_json::Value, + expires_at: Option>, + ) -> Result { + let now = Utc::now(); + + // Check if key already exists + let existing = UserPluginData::find() + .filter(user_plugin_data::Column::UserPluginId.eq(user_plugin_id)) + .filter(user_plugin_data::Column::Key.eq(key)) + .one(db) + .await?; + + match existing { + Some(entry) => { + // Update existing entry + let mut active_model: user_plugin_data::ActiveModel = entry.into(); + active_model.data = Set(data); + active_model.expires_at = Set(expires_at); + active_model.updated_at = Set(now); + + let result = active_model.update(db).await?; + Ok(result) + } + None => { + // Create new entry + let entry = user_plugin_data::ActiveModel { + id: Set(Uuid::new_v4()), + user_plugin_id: Set(user_plugin_id), + key: Set(key.to_string()), + data: Set(data), + expires_at: Set(expires_at), + created_at: Set(now), + updated_at: Set(now), + }; + + let result = entry.insert(db).await?; + Ok(result) + } + } + } + + // ========================================================================= + // Delete Operations + // ========================================================================= + + /// Delete a value by key + /// + /// Returns true if the key existed and was deleted. + pub async fn delete(db: &DatabaseConnection, user_plugin_id: Uuid, key: &str) -> Result { + let result = UserPluginData::delete_many() + .filter(user_plugin_data::Column::UserPluginId.eq(user_plugin_id)) + .filter(user_plugin_data::Column::Key.eq(key)) + .exec(db) + .await?; + Ok(result.rows_affected > 0) + } + + /// Clear all data for a user plugin instance + /// + /// Returns the number of entries deleted. + pub async fn clear_all(db: &DatabaseConnection, user_plugin_id: Uuid) -> Result { + let result = UserPluginData::delete_many() + .filter(user_plugin_data::Column::UserPluginId.eq(user_plugin_id)) + .exec(db) + .await?; + Ok(result.rows_affected) + } + + /// Cleanup expired data across all user plugins + /// + /// This is intended to be called periodically by a background task. + /// Returns the number of expired entries deleted. + pub async fn cleanup_expired(db: &DatabaseConnection) -> Result { + let result = UserPluginData::delete_many() + .filter(user_plugin_data::Column::ExpiresAt.is_not_null()) + .filter(user_plugin_data::Column::ExpiresAt.lte(Utc::now())) + .exec(db) + .await?; + Ok(result.rows_affected) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::entities::plugins; + use crate::db::entities::users; + use crate::db::repositories::{PluginsRepository, UserPluginsRepository, UserRepository}; + use crate::db::test_helpers::setup_test_db; + use chrono::Duration; + + async fn create_test_user(db: &DatabaseConnection) -> users::Model { + let user = users::Model { + id: Uuid::new_v4(), + username: format!("upduser_{}", Uuid::new_v4()), + email: format!("upd_{}@example.com", Uuid::new_v4()), + password_hash: "hash123".to_string(), + role: "reader".to_string(), + is_active: true, + email_verified: false, + permissions: serde_json::json!([]), + created_at: Utc::now(), + updated_at: Utc::now(), + last_login_at: None, + }; + UserRepository::create(db, &user).await.unwrap() + } + + async fn create_test_plugin(db: &DatabaseConnection) -> plugins::Model { + PluginsRepository::create( + db, + &format!("test_plugin_{}", Uuid::new_v4()), + "Test Plugin", + Some("A test user plugin"), + "user", + "node", + vec!["index.js".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "env", + None, + true, + None, + None, + ) + .await + .unwrap() + } + + async fn create_test_user_plugin( + db: &DatabaseConnection, + ) -> ( + users::Model, + plugins::Model, + crate::db::entities::user_plugins::Model, + ) { + let user = create_test_user(db).await; + let plugin = create_test_plugin(db).await; + let user_plugin = UserPluginsRepository::create(db, plugin.id, user.id) + .await + .unwrap(); + (user, plugin, user_plugin) + } + + #[tokio::test] + async fn test_set_and_get() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + let data = serde_json::json!({"score": 0.95, "genres": ["action", "drama"]}); + UserPluginDataRepository::set(&db, user_plugin.id, "taste_profile", data.clone(), None) + .await + .unwrap(); + + let entry = UserPluginDataRepository::get(&db, user_plugin.id, "taste_profile") + .await + .unwrap() + .unwrap(); + + assert_eq!(entry.key, "taste_profile"); + assert_eq!(entry.data, data); + assert!(entry.expires_at.is_none()); + } + + #[tokio::test] + async fn test_set_upsert() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + // Set initial value + let data1 = serde_json::json!({"version": 1}); + UserPluginDataRepository::set(&db, user_plugin.id, "sync_state", data1, None) + .await + .unwrap(); + + // Upsert with new value + let data2 = serde_json::json!({"version": 2}); + UserPluginDataRepository::set(&db, user_plugin.id, "sync_state", data2.clone(), None) + .await + .unwrap(); + + let entry = UserPluginDataRepository::get(&db, user_plugin.id, "sync_state") + .await + .unwrap() + .unwrap(); + + assert_eq!(entry.data, data2); + } + + #[tokio::test] + async fn test_get_nonexistent() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + let entry = UserPluginDataRepository::get(&db, user_plugin.id, "nonexistent") + .await + .unwrap(); + assert!(entry.is_none()); + } + + #[tokio::test] + async fn test_get_expired_auto_deletes() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + // Set a value that has already expired + let data = serde_json::json!({"cached": true}); + let expired_at = Utc::now() - Duration::hours(1); + UserPluginDataRepository::set( + &db, + user_plugin.id, + "recommendations", + data, + Some(expired_at), + ) + .await + .unwrap(); + + // Get should return None and auto-delete + let entry = UserPluginDataRepository::get(&db, user_plugin.id, "recommendations") + .await + .unwrap(); + assert!(entry.is_none()); + } + + #[tokio::test] + async fn test_set_with_ttl() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + let data = serde_json::json!({"recs": [1, 2, 3]}); + let expires_at = Utc::now() + Duration::hours(24); + UserPluginDataRepository::set( + &db, + user_plugin.id, + "recommendations", + data.clone(), + Some(expires_at), + ) + .await + .unwrap(); + + let entry = UserPluginDataRepository::get(&db, user_plugin.id, "recommendations") + .await + .unwrap() + .unwrap(); + + assert_eq!(entry.data, data); + assert!(entry.expires_at.is_some()); + } + + #[tokio::test] + async fn test_list_keys() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + UserPluginDataRepository::set(&db, user_plugin.id, "alpha", serde_json::json!(1), None) + .await + .unwrap(); + UserPluginDataRepository::set(&db, user_plugin.id, "beta", serde_json::json!(2), None) + .await + .unwrap(); + // Add an expired entry that should be excluded + UserPluginDataRepository::set( + &db, + user_plugin.id, + "expired", + serde_json::json!(3), + Some(Utc::now() - Duration::hours(1)), + ) + .await + .unwrap(); + + let keys = UserPluginDataRepository::list_keys(&db, user_plugin.id) + .await + .unwrap(); + + assert_eq!(keys.len(), 2); + assert_eq!(keys[0].key, "alpha"); + assert_eq!(keys[1].key, "beta"); + } + + #[tokio::test] + async fn test_delete_key() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + UserPluginDataRepository::set(&db, user_plugin.id, "to_delete", serde_json::json!(1), None) + .await + .unwrap(); + + let deleted = UserPluginDataRepository::delete(&db, user_plugin.id, "to_delete") + .await + .unwrap(); + assert!(deleted); + + let entry = UserPluginDataRepository::get(&db, user_plugin.id, "to_delete") + .await + .unwrap(); + assert!(entry.is_none()); + + // Deleting non-existent key returns false + let deleted = UserPluginDataRepository::delete(&db, user_plugin.id, "nonexistent") + .await + .unwrap(); + assert!(!deleted); + } + + #[tokio::test] + async fn test_clear_all() { + let db = setup_test_db().await; + let (_, _, user_plugin) = create_test_user_plugin(&db).await; + + UserPluginDataRepository::set(&db, user_plugin.id, "key1", serde_json::json!(1), None) + .await + .unwrap(); + UserPluginDataRepository::set(&db, user_plugin.id, "key2", serde_json::json!(2), None) + .await + .unwrap(); + UserPluginDataRepository::set(&db, user_plugin.id, "key3", serde_json::json!(3), None) + .await + .unwrap(); + + let count = UserPluginDataRepository::clear_all(&db, user_plugin.id) + .await + .unwrap(); + assert_eq!(count, 3); + + let keys = UserPluginDataRepository::list_keys(&db, user_plugin.id) + .await + .unwrap(); + assert!(keys.is_empty()); + } + + #[tokio::test] + async fn test_cleanup_expired() { + let db = setup_test_db().await; + let (_, _, up1) = create_test_user_plugin(&db).await; + + // Create a second user plugin for isolation test + let user2 = create_test_user(&db).await; + let plugin2 = create_test_plugin(&db).await; + let up2 = UserPluginsRepository::create(&db, plugin2.id, user2.id) + .await + .unwrap(); + + // Set expired entries across both user plugins + UserPluginDataRepository::set( + &db, + up1.id, + "expired1", + serde_json::json!(1), + Some(Utc::now() - Duration::hours(1)), + ) + .await + .unwrap(); + UserPluginDataRepository::set( + &db, + up2.id, + "expired2", + serde_json::json!(2), + Some(Utc::now() - Duration::hours(2)), + ) + .await + .unwrap(); + // Non-expired entry should survive + UserPluginDataRepository::set( + &db, + up1.id, + "still_valid", + serde_json::json!(3), + Some(Utc::now() + Duration::hours(24)), + ) + .await + .unwrap(); + // Entry with no expiry should survive + UserPluginDataRepository::set(&db, up1.id, "permanent", serde_json::json!(4), None) + .await + .unwrap(); + + let cleaned = UserPluginDataRepository::cleanup_expired(&db) + .await + .unwrap(); + assert_eq!(cleaned, 2); + + // Verify remaining entries + let keys = UserPluginDataRepository::list_keys(&db, up1.id) + .await + .unwrap(); + assert_eq!(keys.len(), 2); + } + + #[tokio::test] + async fn test_data_isolation_between_user_plugins() { + let db = setup_test_db().await; + let (_, _, up1) = create_test_user_plugin(&db).await; + + let user2 = create_test_user(&db).await; + let plugin2 = create_test_plugin(&db).await; + let up2 = UserPluginsRepository::create(&db, plugin2.id, user2.id) + .await + .unwrap(); + + // Set same key in different user plugins + UserPluginDataRepository::set( + &db, + up1.id, + "shared_key", + serde_json::json!({"owner": "user1"}), + None, + ) + .await + .unwrap(); + UserPluginDataRepository::set( + &db, + up2.id, + "shared_key", + serde_json::json!({"owner": "user2"}), + None, + ) + .await + .unwrap(); + + // Each should see their own data + let data1 = UserPluginDataRepository::get(&db, up1.id, "shared_key") + .await + .unwrap() + .unwrap(); + assert_eq!(data1.data, serde_json::json!({"owner": "user1"})); + + let data2 = UserPluginDataRepository::get(&db, up2.id, "shared_key") + .await + .unwrap() + .unwrap(); + assert_eq!(data2.data, serde_json::json!({"owner": "user2"})); + } + + #[tokio::test] + async fn test_get_by_key_for_user_plugin_ids_empty_input() { + let db = setup_test_db().await; + + let result = UserPluginDataRepository::get_by_key_for_user_plugin_ids(&db, &[], "some_key") + .await + .unwrap(); + + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_get_by_key_for_user_plugin_ids_multiple_plugins() { + let db = setup_test_db().await; + let (_, _, up1) = create_test_user_plugin(&db).await; + let (_, _, up2) = create_test_user_plugin(&db).await; + let (_, _, up3) = create_test_user_plugin(&db).await; + + // Set data for up1 and up2 with the target key + let data1 = serde_json::json!({"value": 1}); + let data2 = serde_json::json!({"value": 2}); + + UserPluginDataRepository::set(&db, up1.id, "sync_state", data1.clone(), None) + .await + .unwrap(); + UserPluginDataRepository::set(&db, up2.id, "sync_state", data2.clone(), None) + .await + .unwrap(); + // up3 doesn't have this key set + + let result = UserPluginDataRepository::get_by_key_for_user_plugin_ids( + &db, + &[up1.id, up2.id, up3.id], + "sync_state", + ) + .await + .unwrap(); + + assert_eq!(result.len(), 2); + assert!(result.contains_key(&up1.id)); + assert!(result.contains_key(&up2.id)); + assert!(!result.contains_key(&up3.id)); + assert_eq!(result.get(&up1.id).unwrap().data, data1); + assert_eq!(result.get(&up2.id).unwrap().data, data2); + } + + #[tokio::test] + async fn test_get_by_key_for_user_plugin_ids_excludes_expired() { + let db = setup_test_db().await; + let (_, _, up1) = create_test_user_plugin(&db).await; + let (_, _, up2) = create_test_user_plugin(&db).await; + + // up1: non-expired data + let data1 = serde_json::json!({"status": "fresh"}); + let future_expiry = Utc::now() + Duration::hours(24); + UserPluginDataRepository::set(&db, up1.id, "cache", data1.clone(), Some(future_expiry)) + .await + .unwrap(); + + // up2: already-expired data + let data2 = serde_json::json!({"status": "stale"}); + let past_expiry = Utc::now() - Duration::hours(1); + UserPluginDataRepository::set(&db, up2.id, "cache", data2, Some(past_expiry)) + .await + .unwrap(); + + let result = UserPluginDataRepository::get_by_key_for_user_plugin_ids( + &db, + &[up1.id, up2.id], + "cache", + ) + .await + .unwrap(); + + // Only up1 should be included (up2's entry is expired) + assert_eq!(result.len(), 1); + assert!(result.contains_key(&up1.id)); + assert!(!result.contains_key(&up2.id)); + assert_eq!(result.get(&up1.id).unwrap().data, data1); + } + + #[tokio::test] + async fn test_get_by_key_for_user_plugin_ids_filters_by_key() { + let db = setup_test_db().await; + let (_, _, up1) = create_test_user_plugin(&db).await; + + // Set multiple keys for the same plugin + UserPluginDataRepository::set(&db, up1.id, "key_a", serde_json::json!({"a": 1}), None) + .await + .unwrap(); + UserPluginDataRepository::set(&db, up1.id, "key_b", serde_json::json!({"b": 2}), None) + .await + .unwrap(); + + // Query for only key_b + let result = + UserPluginDataRepository::get_by_key_for_user_plugin_ids(&db, &[up1.id], "key_b") + .await + .unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result.get(&up1.id).unwrap().key, "key_b"); + assert_eq!( + result.get(&up1.id).unwrap().data, + serde_json::json!({"b": 2}) + ); + } +} diff --git a/src/db/repositories/user_plugins.rs b/src/db/repositories/user_plugins.rs new file mode 100644 index 00000000..acb91a71 --- /dev/null +++ b/src/db/repositories/user_plugins.rs @@ -0,0 +1,819 @@ +//! User Plugins Repository +//! +//! Provides CRUD operations for per-user plugin instances. +//! Handles per-user credentials (encrypted), OAuth tokens, +//! configuration overrides, and health status tracking. +//! +//! ## Key Features +//! +//! - Create, read, update, delete user plugin instances +//! - Per-user encrypted credential storage (simple tokens + OAuth) +//! - OAuth token management (store, refresh, clear) +//! - External identity tracking (username, avatar) +//! - Health status and failure tracking per user +//! - Sync timestamp tracking + +#![allow(dead_code)] + +use crate::db::entities::user_plugins::{self, Entity as UserPlugins}; +use crate::services::CredentialEncryption; +use anyhow::{Result, anyhow}; +use chrono::{DateTime, Utc}; +use sea_orm::*; +use uuid::Uuid; + +pub struct UserPluginsRepository; + +impl UserPluginsRepository { + // ========================================================================= + // Read Operations + // ========================================================================= + + /// Get a user plugin instance by ID + pub async fn get_by_id( + db: &DatabaseConnection, + id: Uuid, + ) -> Result> { + let instance = UserPlugins::find_by_id(id).one(db).await?; + Ok(instance) + } + + /// Get a user's instance of a specific plugin + pub async fn get_by_user_and_plugin( + db: &DatabaseConnection, + user_id: Uuid, + plugin_id: Uuid, + ) -> Result> { + let instance = UserPlugins::find() + .filter(user_plugins::Column::UserId.eq(user_id)) + .filter(user_plugins::Column::PluginId.eq(plugin_id)) + .one(db) + .await?; + Ok(instance) + } + + /// Get all enabled plugin instances for a user + pub async fn get_enabled_for_user( + db: &DatabaseConnection, + user_id: Uuid, + ) -> Result> { + let instances = UserPlugins::find() + .filter(user_plugins::Column::UserId.eq(user_id)) + .filter(user_plugins::Column::Enabled.eq(true)) + .order_by_asc(user_plugins::Column::CreatedAt) + .all(db) + .await?; + Ok(instances) + } + + /// Get all plugin instances for a user (enabled and disabled) + pub async fn get_all_for_user( + db: &DatabaseConnection, + user_id: Uuid, + ) -> Result> { + let instances = UserPlugins::find() + .filter(user_plugins::Column::UserId.eq(user_id)) + .order_by_asc(user_plugins::Column::CreatedAt) + .all(db) + .await?; + Ok(instances) + } + + /// Get all users who have a specific plugin enabled (for broadcast operations) + pub async fn get_users_with_plugin( + db: &DatabaseConnection, + plugin_id: Uuid, + ) -> Result> { + let instances = UserPlugins::find() + .filter(user_plugins::Column::PluginId.eq(plugin_id)) + .filter(user_plugins::Column::Enabled.eq(true)) + .all(db) + .await?; + Ok(instances) + } + + /// Count the number of users who have enabled each plugin. + /// Returns a map from plugin_id to user count (only includes plugins with at least one user). + pub async fn count_users_per_plugin( + db: &DatabaseConnection, + ) -> Result> { + use sea_orm::QuerySelect; + + let results: Vec<(Uuid, i64)> = UserPlugins::find() + .select_only() + .column(user_plugins::Column::PluginId) + .column_as(user_plugins::Column::Id.count(), "user_count") + .group_by(user_plugins::Column::PluginId) + .into_tuple() + .all(db) + .await?; + + Ok(results + .into_iter() + .map(|(plugin_id, count)| (plugin_id, count as u64)) + .collect()) + } + + // ========================================================================= + // Create Operations + // ========================================================================= + + /// Create a new user plugin instance (enable plugin for user) + pub async fn create( + db: &DatabaseConnection, + plugin_id: Uuid, + user_id: Uuid, + ) -> Result { + let now = Utc::now(); + let instance = user_plugins::ActiveModel { + id: Set(Uuid::new_v4()), + plugin_id: Set(plugin_id), + user_id: Set(user_id), + credentials: Set(None), + config: Set(serde_json::json!({})), + oauth_access_token: Set(None), + oauth_refresh_token: Set(None), + oauth_expires_at: Set(None), + oauth_scope: Set(None), + external_user_id: Set(None), + external_username: Set(None), + external_avatar_url: Set(None), + enabled: Set(true), + health_status: Set("unknown".to_string()), + failure_count: Set(0), + last_failure_at: Set(None), + last_success_at: Set(None), + last_sync_at: Set(None), + created_at: Set(now), + updated_at: Set(now), + }; + + let result = instance.insert(db).await?; + Ok(result) + } + + // ========================================================================= + // Update Operations + // ========================================================================= + + /// Update a user plugin instance's configuration + pub async fn update_config( + db: &DatabaseConnection, + id: Uuid, + config: serde_json::Value, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.config = Set(config); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + /// Enable or disable a user plugin instance + pub async fn set_enabled( + db: &DatabaseConnection, + id: Uuid, + enabled: bool, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.enabled = Set(enabled); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + // ========================================================================= + // Credential Operations + // ========================================================================= + + /// Store encrypted simple credentials (API keys, tokens) + pub async fn update_credentials( + db: &DatabaseConnection, + id: Uuid, + credentials: &serde_json::Value, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let encryption = CredentialEncryption::global()?; + let encrypted = encryption.encrypt_json(credentials)?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.credentials = Set(Some(encrypted)); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + /// Decrypt and return simple credentials + pub async fn get_credentials( + db: &DatabaseConnection, + id: Uuid, + ) -> Result> { + let instance = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + match instance.credentials { + Some(encrypted) => { + let encryption = CredentialEncryption::global()?; + let decrypted: serde_json::Value = encryption.decrypt_json(&encrypted)?; + Ok(Some(decrypted)) + } + None => Ok(None), + } + } + + /// Clear credentials + pub async fn clear_credentials( + db: &DatabaseConnection, + id: Uuid, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.credentials = Set(None); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + // ========================================================================= + // OAuth Token Operations + // ========================================================================= + + /// Store encrypted OAuth tokens after successful OAuth flow + pub async fn update_oauth_tokens( + db: &DatabaseConnection, + id: Uuid, + access_token: &str, + refresh_token: Option<&str>, + expires_at: Option>, + scope: Option<&str>, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let encryption = CredentialEncryption::global()?; + let encrypted_access = encryption.encrypt_string(access_token)?; + let encrypted_refresh = match refresh_token { + Some(rt) => Some(encryption.encrypt_string(rt)?), + None => None, + }; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.oauth_access_token = Set(Some(encrypted_access)); + active_model.oauth_refresh_token = Set(encrypted_refresh); + active_model.oauth_expires_at = Set(expires_at); + active_model.oauth_scope = Set(scope.map(|s| s.to_string())); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + /// Decrypt and return OAuth access token + pub async fn get_oauth_access_token( + db: &DatabaseConnection, + id: Uuid, + ) -> Result> { + let instance = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + match instance.oauth_access_token { + Some(encrypted) => { + let encryption = CredentialEncryption::global()?; + let decrypted = encryption.decrypt_string(&encrypted)?; + Ok(Some(decrypted)) + } + None => Ok(None), + } + } + + /// Decrypt and return OAuth refresh token + pub async fn get_oauth_refresh_token( + db: &DatabaseConnection, + id: Uuid, + ) -> Result> { + let instance = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + match instance.oauth_refresh_token { + Some(encrypted) => { + let encryption = CredentialEncryption::global()?; + let decrypted = encryption.decrypt_string(&encrypted)?; + Ok(Some(decrypted)) + } + None => Ok(None), + } + } + + /// Clear all OAuth tokens (disconnect) + pub async fn clear_oauth_tokens( + db: &DatabaseConnection, + id: Uuid, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.oauth_access_token = Set(None); + active_model.oauth_refresh_token = Set(None); + active_model.oauth_expires_at = Set(None); + active_model.oauth_scope = Set(None); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + // ========================================================================= + // External Identity Operations + // ========================================================================= + + /// Update external identity info (from external service) + pub async fn update_external_identity( + db: &DatabaseConnection, + id: Uuid, + external_user_id: Option<&str>, + username: Option<&str>, + avatar_url: Option<&str>, + ) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.external_user_id = Set(external_user_id.map(|s| s.to_string())); + active_model.external_username = Set(username.map(|s| s.to_string())); + active_model.external_avatar_url = Set(avatar_url.map(|s| s.to_string())); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + // ========================================================================= + // Health Status Operations + // ========================================================================= + + /// Record a successful operation + pub async fn record_success(db: &DatabaseConnection, id: Uuid) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.health_status = Set("healthy".to_string()); + active_model.failure_count = Set(0); + active_model.last_success_at = Set(Some(Utc::now())); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + /// Record a failure + pub async fn record_failure(db: &DatabaseConnection, id: Uuid) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let new_failure_count = existing.failure_count + 1; + let health_status = if new_failure_count >= 3 { + "unhealthy" + } else { + "degraded" + }; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.health_status = Set(health_status.to_string()); + active_model.failure_count = Set(new_failure_count); + active_model.last_failure_at = Set(Some(Utc::now())); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + /// Record a sync operation + pub async fn record_sync(db: &DatabaseConnection, id: Uuid) -> Result { + let existing = Self::get_by_id(db, id) + .await? + .ok_or_else(|| anyhow!("User plugin not found: {}", id))?; + + let mut active_model: user_plugins::ActiveModel = existing.into(); + active_model.last_sync_at = Set(Some(Utc::now())); + active_model.updated_at = Set(Utc::now()); + + let result = active_model.update(db).await?; + Ok(result) + } + + // ========================================================================= + // Delete Operations + // ========================================================================= + + /// Delete a user plugin instance (disconnect plugin for user) + pub async fn delete(db: &DatabaseConnection, id: Uuid) -> Result { + let result = UserPlugins::delete_by_id(id).exec(db).await?; + Ok(result.rows_affected > 0) + } + + /// Delete all plugin instances for a user + pub async fn delete_by_user_id(db: &DatabaseConnection, user_id: Uuid) -> Result { + let result = UserPlugins::delete_many() + .filter(user_plugins::Column::UserId.eq(user_id)) + .exec(db) + .await?; + Ok(result.rows_affected) + } + + /// Delete all instances of a plugin (when plugin is removed) + pub async fn delete_by_plugin_id(db: &DatabaseConnection, plugin_id: Uuid) -> Result { + let result = UserPlugins::delete_many() + .filter(user_plugins::Column::PluginId.eq(plugin_id)) + .exec(db) + .await?; + Ok(result.rows_affected) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::entities::plugins; + use crate::db::entities::users; + use crate::db::repositories::PluginsRepository; + use crate::db::repositories::UserRepository; + use crate::db::test_helpers::setup_test_db; + + async fn create_test_user(db: &DatabaseConnection) -> users::Model { + let user = users::Model { + id: Uuid::new_v4(), + username: format!("upuser_{}", Uuid::new_v4()), + email: format!("up_{}@example.com", Uuid::new_v4()), + password_hash: "hash123".to_string(), + role: "reader".to_string(), + is_active: true, + email_verified: false, + permissions: serde_json::json!([]), + created_at: Utc::now(), + updated_at: Utc::now(), + last_login_at: None, + }; + UserRepository::create(db, &user).await.unwrap() + } + + async fn create_test_plugin(db: &DatabaseConnection) -> plugins::Model { + PluginsRepository::create( + db, + &format!("test_plugin_{}", Uuid::new_v4()), + "Test Plugin", + Some("A test user plugin"), + "user", + "node", + vec!["index.js".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "env", + None, + true, + None, + None, + ) + .await + .unwrap() + } + + #[tokio::test] + async fn test_create_user_plugin() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + assert_eq!(instance.plugin_id, plugin.id); + assert_eq!(instance.user_id, user.id); + assert!(instance.enabled); + assert_eq!(instance.health_status, "unknown"); + assert_eq!(instance.failure_count, 0); + assert!(instance.credentials.is_none()); + assert!(instance.oauth_access_token.is_none()); + } + + #[tokio::test] + async fn test_get_by_user_and_plugin() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + // Should not exist initially + let not_found = UserPluginsRepository::get_by_user_and_plugin(&db, user.id, plugin.id) + .await + .unwrap(); + assert!(not_found.is_none()); + + // Create and find + UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + let found = UserPluginsRepository::get_by_user_and_plugin(&db, user.id, plugin.id) + .await + .unwrap() + .unwrap(); + assert_eq!(found.user_id, user.id); + assert_eq!(found.plugin_id, plugin.id); + } + + #[tokio::test] + async fn test_get_enabled_for_user() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin1 = create_test_plugin(&db).await; + let plugin2 = create_test_plugin(&db).await; + + let instance1 = UserPluginsRepository::create(&db, plugin1.id, user.id) + .await + .unwrap(); + UserPluginsRepository::create(&db, plugin2.id, user.id) + .await + .unwrap(); + + // Disable one + UserPluginsRepository::set_enabled(&db, instance1.id, false) + .await + .unwrap(); + + let enabled = UserPluginsRepository::get_enabled_for_user(&db, user.id) + .await + .unwrap(); + assert_eq!(enabled.len(), 1); + assert_eq!(enabled[0].plugin_id, plugin2.id); + } + + #[tokio::test] + async fn test_get_all_for_user() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin1 = create_test_plugin(&db).await; + let plugin2 = create_test_plugin(&db).await; + + UserPluginsRepository::create(&db, plugin1.id, user.id) + .await + .unwrap(); + UserPluginsRepository::create(&db, plugin2.id, user.id) + .await + .unwrap(); + + let all = UserPluginsRepository::get_all_for_user(&db, user.id) + .await + .unwrap(); + assert_eq!(all.len(), 2); + } + + #[tokio::test] + async fn test_get_users_with_plugin() { + let db = setup_test_db().await; + let user1 = create_test_user(&db).await; + let user2 = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + UserPluginsRepository::create(&db, plugin.id, user1.id) + .await + .unwrap(); + UserPluginsRepository::create(&db, plugin.id, user2.id) + .await + .unwrap(); + + let users = UserPluginsRepository::get_users_with_plugin(&db, plugin.id) + .await + .unwrap(); + assert_eq!(users.len(), 2); + } + + #[tokio::test] + async fn test_update_config() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + let config = serde_json::json!({"auto_sync": true, "sync_interval": 3600}); + let updated = UserPluginsRepository::update_config(&db, instance.id, config.clone()) + .await + .unwrap(); + + assert_eq!(updated.config, config); + } + + #[tokio::test] + async fn test_set_enabled() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + assert!(instance.enabled); + + let disabled = UserPluginsRepository::set_enabled(&db, instance.id, false) + .await + .unwrap(); + assert!(!disabled.enabled); + + let enabled = UserPluginsRepository::set_enabled(&db, instance.id, true) + .await + .unwrap(); + assert!(enabled.enabled); + } + + #[tokio::test] + async fn test_update_external_identity() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + let updated = UserPluginsRepository::update_external_identity( + &db, + instance.id, + Some("12345"), + Some("@testuser"), + Some("https://example.com/avatar.png"), + ) + .await + .unwrap(); + + assert_eq!(updated.external_user_id.as_deref(), Some("12345")); + assert_eq!(updated.external_username.as_deref(), Some("@testuser")); + assert_eq!( + updated.external_avatar_url.as_deref(), + Some("https://example.com/avatar.png") + ); + } + + #[tokio::test] + async fn test_record_success() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + let updated = UserPluginsRepository::record_success(&db, instance.id) + .await + .unwrap(); + + assert_eq!(updated.health_status, "healthy"); + assert_eq!(updated.failure_count, 0); + assert!(updated.last_success_at.is_some()); + } + + #[tokio::test] + async fn test_record_failure_escalation() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + // First failure → degraded + let updated = UserPluginsRepository::record_failure(&db, instance.id) + .await + .unwrap(); + assert_eq!(updated.health_status, "degraded"); + assert_eq!(updated.failure_count, 1); + + // Second failure → still degraded + let updated = UserPluginsRepository::record_failure(&db, instance.id) + .await + .unwrap(); + assert_eq!(updated.health_status, "degraded"); + assert_eq!(updated.failure_count, 2); + + // Third failure → unhealthy + let updated = UserPluginsRepository::record_failure(&db, instance.id) + .await + .unwrap(); + assert_eq!(updated.health_status, "unhealthy"); + assert_eq!(updated.failure_count, 3); + } + + #[tokio::test] + async fn test_record_sync() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + assert!(instance.last_sync_at.is_none()); + + let updated = UserPluginsRepository::record_sync(&db, instance.id) + .await + .unwrap(); + assert!(updated.last_sync_at.is_some()); + } + + #[tokio::test] + async fn test_delete() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + let instance = UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + let deleted = UserPluginsRepository::delete(&db, instance.id) + .await + .unwrap(); + assert!(deleted); + + let not_found = UserPluginsRepository::get_by_id(&db, instance.id) + .await + .unwrap(); + assert!(not_found.is_none()); + } + + #[tokio::test] + async fn test_delete_by_user_id() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin1 = create_test_plugin(&db).await; + let plugin2 = create_test_plugin(&db).await; + + UserPluginsRepository::create(&db, plugin1.id, user.id) + .await + .unwrap(); + UserPluginsRepository::create(&db, plugin2.id, user.id) + .await + .unwrap(); + + let deleted_count = UserPluginsRepository::delete_by_user_id(&db, user.id) + .await + .unwrap(); + assert_eq!(deleted_count, 2); + + let remaining = UserPluginsRepository::get_all_for_user(&db, user.id) + .await + .unwrap(); + assert!(remaining.is_empty()); + } + + #[tokio::test] + async fn test_unique_constraint() { + let db = setup_test_db().await; + let user = create_test_user(&db).await; + let plugin = create_test_plugin(&db).await; + + // First creation should succeed + UserPluginsRepository::create(&db, plugin.id, user.id) + .await + .unwrap(); + + // Second creation for same user+plugin should fail + let result = UserPluginsRepository::create(&db, plugin.id, user.id).await; + assert!(result.is_err()); + } +} diff --git a/src/services/metadata/apply.rs b/src/services/metadata/apply.rs index 2053e100..170db36b 100644 --- a/src/services/metadata/apply.rs +++ b/src/services/metadata/apply.rs @@ -17,7 +17,7 @@ use crate::db::entities::plugins::{Model as Plugin, PluginPermission}; use crate::db::entities::series_metadata::Model as SeriesMetadata; use crate::db::repositories::{ AlternateTitleRepository, ExternalLinkRepository, ExternalRatingRepository, GenreRepository, - SeriesMetadataRepository, TagRepository, + SeriesExternalIdRepository, SeriesMetadataRepository, TagRepository, }; use crate::events::EventBroadcaster; use crate::services::ThumbnailService; @@ -435,6 +435,30 @@ impl MetadataApplier { } } + // External IDs (cross-references to other services) + if should_apply_field("externalIds") && !metadata.external_ids.is_empty() { + if !plugin.has_permission(&PluginPermission::MetadataWriteExternalIds) { + skipped_fields.push(SkippedField { + field: "externalIds".to_string(), + reason: "Plugin does not have permission".to_string(), + }); + } else { + for ext_id in &metadata.external_ids { + SeriesExternalIdRepository::upsert( + db, + series_id, + &ext_id.source, + &ext_id.external_id, + None, // external_url - not provided in cross-references + None, // metadata_hash - not applicable for cross-references + ) + .await + .context("Failed to upsert external ID")?; + } + applied_fields.push("externalIds".to_string()); + } + } + // External Ratings (primary rating from plugin) if should_apply_field("rating") && let Some(rating) = &metadata.rating diff --git a/src/services/mod.rs b/src/services/mod.rs index c74f3913..11bb325a 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -15,6 +15,7 @@ pub mod settings; pub mod task_listener; pub mod task_metrics; pub mod thumbnail; +pub mod user_plugin; pub use auth_tracking::AuthTrackingService; pub use cleanup_subscriber::CleanupEventSubscriber; diff --git a/src/services/plugin/encryption.rs b/src/services/plugin/encryption.rs index 32ef27b2..1bc26e3f 100644 --- a/src/services/plugin/encryption.rs +++ b/src/services/plugin/encryption.rs @@ -25,7 +25,6 @@ pub struct CredentialEncryption { cipher: Aes256Gcm, } -#[allow(dead_code)] impl CredentialEncryption { /// Create a new encryption service with the given 256-bit key pub fn new(key: &[u8; 32]) -> Self { @@ -140,6 +139,7 @@ impl CredentialEncryption { } /// Generate a new random encryption key (32 bytes) + #[allow(dead_code)] // Operational utility: used by administrators for key generation pub fn generate_key() -> [u8; 32] { let mut key = [0u8; 32]; rand::thread_rng().fill_bytes(&mut key); @@ -147,6 +147,7 @@ impl CredentialEncryption { } /// Generate a new random encryption key and encode as base64 + #[allow(dead_code)] // Operational utility: used by administrators for key generation pub fn generate_key_base64() -> String { BASE64.encode(Self::generate_key()) } diff --git a/src/services/plugin/handle.rs b/src/services/plugin/handle.rs index c4809f8c..65b37c6f 100644 --- a/src/services/plugin/handle.rs +++ b/src/services/plugin/handle.rs @@ -2,13 +2,6 @@ //! //! This module provides the `PluginHandle` which manages a single plugin's lifecycle, //! including initialization, request handling, health tracking, and shutdown. -//! -//! Note: Some methods and error variants are designed for the complete plugin API -//! but may not be called from external code yet. - -// Allow dead code for plugin API methods and error variants that are part of the -// complete API surface but not yet called from external code. -#![allow(dead_code)] use std::sync::Arc; use std::time::Duration; @@ -19,7 +12,7 @@ use serde_json::Value; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; -use super::health::{HealthState, HealthTracker}; +use super::health::HealthTracker; use super::process::{PluginProcess, PluginProcessConfig, ProcessError}; use super::protocol::{ InitializeParams, MetadataGetParams, MetadataMatchParams, MetadataSearchParams, @@ -28,6 +21,7 @@ use super::protocol::{ }; use super::rpc::{RpcClient, RpcError}; use super::secrets::SecretValue; +use super::storage_handler::StorageRequestHandler; /// Error type for plugin handle operations #[derive(Debug, thiserror::Error)] @@ -44,14 +38,8 @@ pub enum PluginError { #[error("Plugin is disabled: {reason}")] Disabled { reason: String }, - #[error("Plugin health check failed: {0}")] - HealthCheckFailed(String), - #[error("Plugin spawn failed: {0}")] SpawnFailed(String), - - #[error("Invalid manifest: {0}")] - InvalidManifest(String), } /// Configuration for a plugin handle @@ -69,8 +57,10 @@ pub struct PluginConfig { pub shutdown_timeout: Duration, /// Maximum consecutive failures before disabling pub max_failures: u32, - /// Initial configuration to pass to plugin - pub config: Option, + /// Admin-level configuration (from plugin settings) + pub admin_config: Option, + /// Per-user configuration (from user plugin settings) + pub user_config: Option, /// Credentials to pass to plugin (via init message) /// Uses SecretValue to prevent logging of sensitive data pub credentials: Option, @@ -83,7 +73,8 @@ impl std::fmt::Debug for PluginConfig { .field("request_timeout", &self.request_timeout) .field("shutdown_timeout", &self.shutdown_timeout) .field("max_failures", &self.max_failures) - .field("config", &self.config) + .field("admin_config", &self.admin_config) + .field("user_config", &self.user_config) .field("credentials", &self.credentials) // SecretValue shows [REDACTED] .finish() } @@ -96,7 +87,8 @@ impl Default for PluginConfig { request_timeout: Duration::from_secs(30), shutdown_timeout: Duration::from_secs(5), max_failures: 3, - config: None, + admin_config: None, + user_config: None, credentials: None, } } @@ -131,6 +123,8 @@ pub struct PluginHandle { manifest: Arc>>, /// Health tracker health: Arc, + /// Optional storage handler for user plugin reverse RPC + storage_handler: Option, } impl PluginHandle { @@ -142,6 +136,22 @@ impl PluginHandle { state: Arc::new(RwLock::new(PluginState::Idle)), client: Arc::new(RwLock::new(None)), manifest: Arc::new(RwLock::new(None)), + storage_handler: None, + } + } + + /// Create a new plugin handle with storage support for user plugins. + /// + /// The storage handler enables the plugin to make `storage/*` reverse RPC + /// calls that are handled by the host using the database. + pub fn new_with_storage(config: PluginConfig, storage_handler: StorageRequestHandler) -> Self { + Self { + health: Arc::new(HealthTracker::new(config.max_failures)), + config, + state: Arc::new(RwLock::new(PluginState::Idle)), + client: Arc::new(RwLock::new(None)), + manifest: Arc::new(RwLock::new(None)), + storage_handler: Some(storage_handler), } } @@ -155,21 +165,6 @@ impl PluginHandle { self.manifest.read().await.clone() } - /// Get the health state - pub async fn health_state(&self) -> HealthState { - self.health.state().await - } - - /// Check if the plugin is currently running - pub async fn is_running(&self) -> bool { - matches!(*self.state.read().await, PluginState::Running) - } - - /// Check if the plugin is disabled - pub async fn is_disabled(&self) -> bool { - matches!(*self.state.read().await, PluginState::Disabled { .. }) - } - /// Spawn the plugin process and initialize it pub async fn start(&self) -> Result { // Check if already running @@ -223,19 +218,41 @@ impl PluginHandle { } }; - // Create RPC client - let mut client = RpcClient::new(process, self.config.request_timeout); + // Create RPC client (with storage support if configured) + let mut client = match &self.storage_handler { + Some(handler) => { + debug!("Creating RPC client with storage handler for user plugin"); + RpcClient::new_with_storage(process, self.config.request_timeout, handler.clone()) + } + None => RpcClient::new(process, self.config.request_timeout), + }; debug!("RPC client created, sending initialize request"); // Initialize the plugin - // Convert SecretValue to Value for the init message + // Build merged config for backward compat, and send split configs + let merged_config = match (&self.config.admin_config, &self.config.user_config) { + (Some(admin), Some(user)) => { + let mut merged = admin.clone(); + if let (Some(base), Some(overlay)) = (merged.as_object_mut(), user.as_object()) { + for (k, v) in overlay { + base.insert(k.clone(), v.clone()); + } + } + Some(merged) + } + (Some(c), None) | (None, Some(c)) => Some(c.clone()), + (None, None) => None, + }; let init_params = InitializeParams { - config: self.config.config.clone(), + config: merged_config, + admin_config: self.config.admin_config.clone(), + user_config: self.config.user_config.clone(), credentials: self.config.credentials.as_ref().map(|s| s.inner().clone()), }; debug!( - has_config = init_params.config.is_some(), + has_admin_config = init_params.admin_config.is_some(), + has_user_config = init_params.user_config.is_some(), has_credentials = init_params.credentials.is_some(), "Sending initialize request to plugin" ); @@ -321,12 +338,6 @@ impl PluginHandle { Ok(()) } - /// Restart the plugin - pub async fn restart(&self) -> Result { - self.stop().await?; - self.start().await - } - /// Send a ping to check if the plugin is responsive pub async fn ping(&self) -> Result<(), PluginError> { self.ensure_running().await?; @@ -512,23 +523,6 @@ impl PluginHandle { } } - /// Re-enable a disabled plugin - pub async fn enable(&self) -> Result<(), PluginError> { - let current_state = self.state.read().await.clone(); - - if let PluginState::Disabled { reason: _ } = current_state { - self.health.reset().await; - { - let mut state = self.state.write().await; - *state = PluginState::Idle; - } - info!("Plugin re-enabled"); - Ok(()) - } else { - Ok(()) // Already enabled - } - } - /// Ensure the plugin is in a running state async fn ensure_running(&self) -> Result<(), PluginError> { let state = self.state.read().await.clone(); @@ -596,21 +590,6 @@ mod tests { let handle = PluginHandle::new(config); assert_eq!(handle.state().await, PluginState::Idle); - assert!(!handle.is_running().await); - assert!(!handle.is_disabled().await); assert!(handle.manifest().await.is_none()); } - - #[tokio::test] - async fn test_plugin_handle_enable_when_not_disabled() { - let config = PluginConfig::default(); - let handle = PluginHandle::new(config); - - // Should be a no-op when not disabled - handle.enable().await.unwrap(); - assert_eq!(handle.state().await, PluginState::Idle); - } - - // Integration tests would require a mock plugin process - // See tests/integration/plugin_handle.rs for full integration tests } diff --git a/src/services/plugin/health.rs b/src/services/plugin/health.rs index 2726d55c..d31dcfd7 100644 --- a/src/services/plugin/health.rs +++ b/src/services/plugin/health.rs @@ -1,289 +1,61 @@ -//! Health Monitoring for Plugins +//! Health Tracking for Plugins //! -//! This module provides health tracking and monitoring for plugins, -//! including failure counting and auto-disable logic. -//! -//! Note: This module provides complete health monitoring infrastructure. -//! Some types and methods may not be called from external code yet but are -//! part of the complete API for plugin health management. - -// Allow dead code for health monitoring infrastructure that is part of the -// complete API surface but not yet fully integrated. -#![allow(dead_code)] +//! Tracks consecutive failures for plugins and determines when a plugin +//! should be auto-disabled. Used by `PluginHandle` for in-process health +//! decisions during active operations. -use chrono::{DateTime, Utc}; use std::sync::atomic::{AtomicU32, Ordering}; -use tokio::sync::RwLock; - -/// Health status of a plugin -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum HealthStatus { - /// Plugin is healthy - Healthy, - /// Plugin is degraded (some failures but still operational) - Degraded, - /// Plugin is unhealthy (at or near failure threshold) - Unhealthy, - /// Plugin health is unknown (not yet checked) - Unknown, - /// Plugin is disabled due to failures - Disabled, -} - -impl HealthStatus { - pub fn as_str(&self) -> &str { - match self { - HealthStatus::Healthy => "healthy", - HealthStatus::Degraded => "degraded", - HealthStatus::Unhealthy => "unhealthy", - HealthStatus::Unknown => "unknown", - HealthStatus::Disabled => "disabled", - } - } -} - -impl std::fmt::Display for HealthStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// Current health state of a plugin -#[derive(Debug, Clone)] -pub struct HealthState { - /// Current health status - pub status: HealthStatus, - /// Number of consecutive failures - pub consecutive_failures: u32, - /// Total failure count (lifetime) - pub total_failures: u32, - /// Total success count (lifetime) - pub total_successes: u32, - /// Last successful operation - pub last_success_at: Option>, - /// Last failed operation - pub last_failure_at: Option>, - /// Reason for current status (if applicable) - pub reason: Option, -} - -impl Default for HealthState { - fn default() -> Self { - Self { - status: HealthStatus::Unknown, - consecutive_failures: 0, - total_failures: 0, - total_successes: 0, - last_success_at: None, - last_failure_at: None, - reason: None, - } - } -} -/// Tracks health state for a plugin +/// Tracks consecutive failure count for a plugin to support auto-disable logic. pub struct HealthTracker { /// Maximum consecutive failures before disabling max_failures: u32, /// Number of consecutive failures consecutive_failures: AtomicU32, - /// Total failure count - total_failures: AtomicU32, - /// Total success count - total_successes: AtomicU32, - /// Last success time - last_success_at: RwLock>>, - /// Last failure time - last_failure_at: RwLock>>, - /// Whether the plugin has been disabled - disabled: RwLock, - /// Reason for being disabled - disabled_reason: RwLock>, } impl HealthTracker { - /// Create a new health tracker + /// Create a new health tracker with the given failure threshold. pub fn new(max_failures: u32) -> Self { Self { max_failures, consecutive_failures: AtomicU32::new(0), - total_failures: AtomicU32::new(0), - total_successes: AtomicU32::new(0), - last_success_at: RwLock::new(None), - last_failure_at: RwLock::new(None), - disabled: RwLock::new(false), - disabled_reason: RwLock::new(None), } } - /// Record a successful operation + /// Record a successful operation (resets consecutive failure count). pub async fn record_success(&self) { self.consecutive_failures.store(0, Ordering::SeqCst); - self.total_successes.fetch_add(1, Ordering::SeqCst); - *self.last_success_at.write().await = Some(Utc::now()); } - /// Record a failed operation + /// Record a failed operation (increments consecutive failure count). pub async fn record_failure(&self) { self.consecutive_failures.fetch_add(1, Ordering::SeqCst); - self.total_failures.fetch_add(1, Ordering::SeqCst); - *self.last_failure_at.write().await = Some(Utc::now()); } - /// Check if the plugin should be disabled due to failures + /// Check if the plugin should be disabled due to reaching the failure threshold. pub async fn should_disable(&self) -> bool { let failures = self.consecutive_failures.load(Ordering::SeqCst); failures >= self.max_failures } - - /// Mark the plugin as disabled - pub async fn mark_disabled(&self, reason: impl Into) { - *self.disabled.write().await = true; - *self.disabled_reason.write().await = Some(reason.into()); - } - - /// Check if the plugin is disabled - pub async fn is_disabled(&self) -> bool { - *self.disabled.read().await - } - - /// Reset the health tracker (for re-enabling) - pub async fn reset(&self) { - self.consecutive_failures.store(0, Ordering::SeqCst); - *self.disabled.write().await = false; - *self.disabled_reason.write().await = None; - } - - /// Get the current health state - pub async fn state(&self) -> HealthState { - let consecutive_failures = self.consecutive_failures.load(Ordering::SeqCst); - let total_failures = self.total_failures.load(Ordering::SeqCst); - let total_successes = self.total_successes.load(Ordering::SeqCst); - let last_success_at = *self.last_success_at.read().await; - let last_failure_at = *self.last_failure_at.read().await; - let is_disabled = *self.disabled.read().await; - let disabled_reason = self.disabled_reason.read().await.clone(); - - let status = if is_disabled { - HealthStatus::Disabled - } else if total_successes == 0 && total_failures == 0 { - HealthStatus::Unknown - } else if consecutive_failures >= self.max_failures { - HealthStatus::Unhealthy - } else if consecutive_failures > 0 { - HealthStatus::Degraded - } else { - HealthStatus::Healthy - }; - - HealthState { - status, - consecutive_failures, - total_failures, - total_successes, - last_success_at, - last_failure_at, - reason: disabled_reason, - } - } - - /// Get the current health status - pub async fn status(&self) -> HealthStatus { - self.state().await.status - } -} - -/// Monitor for managing health checks across multiple plugins -pub struct HealthMonitor { - /// Check interval - check_interval: std::time::Duration, - /// Whether monitoring is active - active: RwLock, -} - -impl HealthMonitor { - /// Create a new health monitor - pub fn new(check_interval: std::time::Duration) -> Self { - Self { - check_interval, - active: RwLock::new(false), - } - } - - /// Get the check interval - pub fn check_interval(&self) -> std::time::Duration { - self.check_interval - } - - /// Check if monitoring is active - pub async fn is_active(&self) -> bool { - *self.active.read().await - } - - /// Start monitoring (placeholder - actual implementation in Phase 2) - pub async fn start(&self) { - *self.active.write().await = true; - } - - /// Stop monitoring - pub async fn stop(&self) { - *self.active.write().await = false; - } } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_health_status_as_str() { - assert_eq!(HealthStatus::Healthy.as_str(), "healthy"); - assert_eq!(HealthStatus::Degraded.as_str(), "degraded"); - assert_eq!(HealthStatus::Unhealthy.as_str(), "unhealthy"); - assert_eq!(HealthStatus::Unknown.as_str(), "unknown"); - assert_eq!(HealthStatus::Disabled.as_str(), "disabled"); - } - - #[test] - fn test_health_status_display() { - assert_eq!(format!("{}", HealthStatus::Healthy), "healthy"); - assert_eq!(format!("{}", HealthStatus::Disabled), "disabled"); - } - - #[test] - fn test_health_state_default() { - let state = HealthState::default(); - assert_eq!(state.status, HealthStatus::Unknown); - assert_eq!(state.consecutive_failures, 0); - assert_eq!(state.total_failures, 0); - assert_eq!(state.total_successes, 0); - assert!(state.last_success_at.is_none()); - assert!(state.last_failure_at.is_none()); - assert!(state.reason.is_none()); - } - - #[tokio::test] - async fn test_health_tracker_initial_state() { - let tracker = HealthTracker::new(3); - let state = tracker.state().await; - - assert_eq!(state.status, HealthStatus::Unknown); - assert_eq!(state.consecutive_failures, 0); - assert_eq!(state.total_failures, 0); - assert_eq!(state.total_successes, 0); - } - #[tokio::test] async fn test_health_tracker_record_success() { let tracker = HealthTracker::new(3); - tracker.record_success().await; - let state = tracker.state().await; + // Initially no failures + assert!(!tracker.should_disable().await); - assert_eq!(state.status, HealthStatus::Healthy); - assert_eq!(state.consecutive_failures, 0); - assert_eq!(state.total_successes, 1); - assert!(state.last_success_at.is_some()); + // Record failures then success resets count + tracker.record_failure().await; + tracker.record_failure().await; + tracker.record_success().await; + assert!(!tracker.should_disable().await); } #[tokio::test] @@ -291,88 +63,39 @@ mod tests { let tracker = HealthTracker::new(3); tracker.record_failure().await; - let state = tracker.state().await; - - assert_eq!(state.status, HealthStatus::Degraded); - assert_eq!(state.consecutive_failures, 1); - assert_eq!(state.total_failures, 1); - assert!(state.last_failure_at.is_some()); - } - - #[tokio::test] - async fn test_health_tracker_success_resets_failures() { - let tracker = HealthTracker::new(3); + assert!(!tracker.should_disable().await); - // Record some failures tracker.record_failure().await; - tracker.record_failure().await; - assert_eq!(tracker.state().await.consecutive_failures, 2); - - // Success should reset consecutive failures - tracker.record_success().await; - assert_eq!(tracker.state().await.consecutive_failures, 0); - assert_eq!(tracker.state().await.total_failures, 2); // Total unchanged + assert!(!tracker.should_disable().await); } #[tokio::test] - async fn test_health_tracker_should_disable() { + async fn test_health_tracker_should_disable_at_threshold() { let tracker = HealthTracker::new(3); - // Not enough failures yet tracker.record_failure().await; tracker.record_failure().await; assert!(!tracker.should_disable().await); - // Third failure should trigger disable + // Third failure hits the threshold tracker.record_failure().await; assert!(tracker.should_disable().await); - assert_eq!(tracker.state().await.status, HealthStatus::Unhealthy); - } - - #[tokio::test] - async fn test_health_tracker_mark_disabled() { - let tracker = HealthTracker::new(3); - - tracker.mark_disabled("Too many failures").await; - let state = tracker.state().await; - - assert_eq!(state.status, HealthStatus::Disabled); - assert!(tracker.is_disabled().await); - assert_eq!(state.reason, Some("Too many failures".to_string())); } #[tokio::test] - async fn test_health_tracker_reset() { + async fn test_health_tracker_success_resets_failures() { let tracker = HealthTracker::new(3); - // Add some failures and disable - tracker.record_failure().await; + // Get close to threshold tracker.record_failure().await; tracker.record_failure().await; - tracker.mark_disabled("Test").await; - assert!(tracker.is_disabled().await); - - // Reset should clear disabled state - tracker.reset().await; - - assert!(!tracker.is_disabled().await); - assert_eq!(tracker.state().await.consecutive_failures, 0); - // Note: total_failures is NOT reset - assert_eq!(tracker.state().await.total_failures, 3); - } - - #[tokio::test] - async fn test_health_monitor_lifecycle() { - let monitor = HealthMonitor::new(std::time::Duration::from_secs(30)); - - assert!(!monitor.is_active().await); - assert_eq!(monitor.check_interval(), std::time::Duration::from_secs(30)); - - monitor.start().await; - assert!(monitor.is_active().await); + // Success resets + tracker.record_success().await; + assert!(!tracker.should_disable().await); - monitor.stop().await; - assert!(!monitor.is_active().await); + // Need 3 fresh failures to hit threshold again + tracker.record_failure().await; + assert!(!tracker.should_disable().await); } } diff --git a/src/services/plugin/library.rs b/src/services/plugin/library.rs new file mode 100644 index 00000000..717b8d03 --- /dev/null +++ b/src/services/plugin/library.rs @@ -0,0 +1,192 @@ +//! User Library Builder +//! +//! Builds `Vec` from a user's Codex library data for +//! sending to recommendation plugins. Uses batch queries for efficiency. + +use anyhow::Result; +use sea_orm::DatabaseConnection; +use std::collections::HashMap; +use tracing::{debug, warn}; +use uuid::Uuid; + +use crate::db::entities::SeriesStatus; +use crate::db::repositories::{ + AlternateTitleRepository, BookRepository, GenreRepository, ReadProgressRepository, + SeriesExternalIdRepository, SeriesMetadataRepository, SeriesRepository, TagRepository, + UserSeriesRatingRepository, +}; +use crate::services::plugin::protocol::{ + UserLibraryEntry, UserLibraryExternalId, UserReadingStatus, +}; + +/// Build the full user library as `Vec` for recommendation plugins. +/// +/// Fetches all series, metadata, genres, tags, external IDs, reading progress, +/// and user ratings in batch, then assembles them into library entries. +pub async fn build_user_library( + db: &DatabaseConnection, + user_id: Uuid, +) -> Result> { + // 1. Get all series + let all_series = SeriesRepository::list_all(db).await?; + if all_series.is_empty() { + return Ok(vec![]); + } + + let series_ids: Vec = all_series.iter().map(|s| s.id).collect(); + + // 2. Batch-fetch all related data + let metadata_map = SeriesMetadataRepository::get_by_series_ids(db, &series_ids).await?; + let genres_map = GenreRepository::get_genres_for_series_ids(db, &series_ids).await?; + let tags_map = TagRepository::get_tags_for_series_ids(db, &series_ids).await?; + let ext_ids_map = SeriesExternalIdRepository::get_for_series_ids(db, &series_ids).await?; + let alt_titles_map = AlternateTitleRepository::get_for_series_ids(db, &series_ids).await?; + + // 3. Batch-fetch all books and reading progress + let all_books = BookRepository::list_by_series_ids(db, &series_ids).await?; + let mut books_by_series: HashMap> = HashMap::new(); + for book in &all_books { + books_by_series + .entry(book.series_id) + .or_default() + .push(book.id); + } + + let all_progress = ReadProgressRepository::get_by_user(db, user_id).await?; + let progress_by_book: HashMap = + all_progress.into_iter().map(|p| (p.book_id, p)).collect(); + + // 4. Batch-fetch user ratings + let ratings_map: HashMap = + match UserSeriesRatingRepository::get_all_for_user(db, user_id).await { + Ok(ratings) => ratings.into_iter().map(|r| (r.series_id, r)).collect(), + Err(e) => { + warn!("Failed to fetch user ratings: {}", e); + HashMap::new() + } + }; + + // 5. Build entries + let mut entries = Vec::new(); + + for series in &all_series { + let meta = metadata_map.get(&series.id); + let title = meta + .map(|m| m.title.clone()) + .unwrap_or_else(|| series.name.clone()); + + let book_ids = books_by_series.get(&series.id); + let books_owned = book_ids.map(|b| b.len() as i32).unwrap_or(0); + + // Aggregate reading progress + let mut books_read = 0i32; + let mut earliest_started: Option> = None; + let mut latest_read_at: Option> = None; + let mut latest_completed_at: Option> = None; + + if let Some(book_ids) = book_ids { + for book_id in book_ids { + if let Some(progress) = progress_by_book.get(book_id) { + if progress.completed { + books_read += 1; + if let Some(cat) = progress.completed_at { + latest_completed_at = Some(match latest_completed_at { + Some(existing) if cat > existing => cat, + Some(existing) => existing, + None => cat, + }); + } + } + earliest_started = Some(match earliest_started { + Some(existing) if progress.started_at < existing => progress.started_at, + Some(existing) => existing, + None => progress.started_at, + }); + latest_read_at = Some(match latest_read_at { + Some(existing) if progress.updated_at > existing => progress.updated_at, + Some(existing) => existing, + None => progress.updated_at, + }); + } + } + } + + // Derive reading status + let reading_status = if books_read == 0 { + Some(UserReadingStatus::Unread) + } else if books_read >= books_owned && books_owned > 0 { + Some(UserReadingStatus::Completed) + } else { + Some(UserReadingStatus::Reading) + }; + + // Genres and tags as string names + let genres = genres_map + .get(&series.id) + .map(|gs| gs.iter().map(|g| g.name.clone()).collect()) + .unwrap_or_default(); + + let tags = tags_map + .get(&series.id) + .map(|ts| ts.iter().map(|t| t.name.clone()).collect()) + .unwrap_or_default(); + + // External IDs + let external_ids = ext_ids_map + .get(&series.id) + .map(|eids| { + eids.iter() + .map(|e| UserLibraryExternalId { + source: e.source.clone(), + external_id: e.external_id.clone(), + external_url: e.external_url.clone(), + }) + .collect() + }) + .unwrap_or_default(); + + // Alternate titles + let alternate_titles = alt_titles_map + .get(&series.id) + .map(|alts| alts.iter().map(|a| a.title.clone()).collect()) + .unwrap_or_default(); + + // User rating/notes + let (user_rating, user_notes) = match ratings_map.get(&series.id) { + Some(r) => (Some(r.rating), r.notes.clone()), + None => (None, None), + }; + + entries.push(UserLibraryEntry { + series_id: series.id.to_string(), + title, + alternate_titles, + year: meta.and_then(|m| m.year), + status: meta.and_then(|m| { + m.status + .as_deref() + .and_then(|s| s.parse::().ok()) + }), + genres, + tags, + total_book_count: meta.and_then(|m| m.total_book_count), + external_ids, + reading_status, + books_read, + books_owned, + user_rating, + user_notes, + started_at: earliest_started.map(|dt| dt.to_rfc3339()), + last_read_at: latest_read_at.map(|dt| dt.to_rfc3339()), + completed_at: latest_completed_at.map(|dt| dt.to_rfc3339()), + }); + } + + debug!( + "Built {} user library entries for user {}", + entries.len(), + user_id + ); + + Ok(entries) +} diff --git a/src/services/plugin/manager.rs b/src/services/plugin/manager.rs index 1e284834..41d816d3 100644 --- a/src/services/plugin/manager.rs +++ b/src/services/plugin/manager.rs @@ -33,13 +33,8 @@ //! └───────────────────────────────────────────────────────────────────┘ //! ``` //! -//! Note: This module provides complete plugin management infrastructure. -//! Some methods and error variants may not be called from external code yet -//! but are part of the complete API for plugin lifecycle management. - -// Allow dead code for plugin management infrastructure that is part of the -// complete API surface but not yet fully integrated. -#![allow(dead_code)] +//! Note: Some methods and error variants are part of the complete API +//! surface but not yet called from external code. use std::collections::HashMap; use std::sync::Arc; @@ -52,17 +47,23 @@ use tracing::{debug, error, info, warn}; use uuid::Uuid; use crate::db::entities::plugins; -use crate::db::repositories::{FailureContext, PluginFailuresRepository, PluginsRepository}; +use crate::db::entities::user_plugins; +use crate::db::repositories::{ + FailureContext, PluginFailuresRepository, PluginsRepository, UserPluginsRepository, +}; use crate::services::PluginMetricsService; -use super::handle::{PluginConfig, PluginError, PluginHandle}; +use crate::services::user_plugin::token_refresh::{self, RefreshResult}; + +use super::handle::{PluginConfig, PluginError, PluginHandle, PluginState}; use super::process::PluginProcessConfig; use super::protocol::{ BookMatchParams, BookSearchParams, MetadataGetParams, MetadataMatchParams, - MetadataSearchParams, MetadataSearchResponse, PluginBookMetadata, PluginScope, - PluginSeriesMetadata, SearchResult, + MetadataSearchParams, MetadataSearchResponse, OAuthConfig, PluginBookMetadata, PluginManifest, + PluginScope, PluginSeriesMetadata, SearchResult, }; use super::secrets::SecretValue; +use super::storage_handler::StorageRequestHandler; /// Error type for plugin manager operations #[derive(Debug, thiserror::Error)] @@ -82,14 +83,30 @@ pub enum PluginManagerError { #[error("Encryption error: {0}")] Encryption(String), - #[error("No plugins available for scope: {0:?}")] - NoPluginsForScope(PluginScope), - #[error("Rate limit exceeded for plugin {plugin_id}: {requests_per_minute} requests/minute")] RateLimited { plugin_id: Uuid, requests_per_minute: i32, }, + + #[error("User plugin not found for user {user_id} and plugin {plugin_id}")] + UserPluginNotFound { user_id: Uuid, plugin_id: Uuid }, + + #[error("OAuth token refresh failed: {0}")] + TokenRefreshFailed(String), + + #[error("OAuth re-authentication required for user plugin {0}")] + ReauthRequired(Uuid), +} + +/// Context for a user plugin operation +/// +/// Tracks the user and their plugin instance, used for scoping +/// storage operations and credential injection. +#[derive(Debug, Clone)] +pub struct UserPluginContext { + /// The user plugin instance ID (from `user_plugins` table) + pub user_plugin_id: Uuid, } /// Configuration for the plugin manager @@ -327,11 +344,6 @@ impl PluginManager { self } - /// Get a reference to the metrics service if configured - pub fn metrics_service(&self) -> Option<&Arc> { - self.metrics_service.as_ref() - } - /// Load all enabled plugins from database pub async fn load_all(&self) -> Result { debug!("Loading enabled plugins from database..."); @@ -492,7 +504,7 @@ impl PluginManager { } if let Some(ref handle) = entry.handle - && handle.is_running().await + && handle.state().await == PluginState::Running { return Ok(Arc::clone(handle)); } @@ -527,7 +539,7 @@ impl PluginManager { } if let Some(ref handle) = entry.handle - && handle.is_running().await + && handle.state().await == PluginState::Running { return Ok(Arc::clone(handle)); } @@ -640,16 +652,251 @@ impl PluginManager { .collect() } - /// Get a specific plugin's database configuration - pub async fn get_plugin(&self, plugin_id: Uuid) -> Option { - let plugins = self.plugins.read().await; - plugins.get(&plugin_id).map(|e| e.db_config.clone()) + // ========================================================================= + // User Plugin Methods + // ========================================================================= + + /// Get or spawn a plugin handle for a specific user + /// + /// This method looks up the user's plugin instance from the database, + /// decrypts their credentials, and spawns a plugin handle with per-user + /// credential injection. + /// + /// Returns the plugin handle and user plugin context (for storage scoping). + pub async fn get_user_plugin_handle( + &self, + plugin_id: Uuid, + user_id: Uuid, + ) -> Result<(Arc, UserPluginContext), PluginManagerError> { + // Look up the user's plugin instance + let user_plugin = + UserPluginsRepository::get_by_user_and_plugin(&self.db, user_id, plugin_id) + .await? + .ok_or(PluginManagerError::UserPluginNotFound { user_id, plugin_id })?; + + if !user_plugin.enabled { + return Err(PluginManagerError::PluginNotEnabled(plugin_id)); + } + + let context = UserPluginContext { + user_plugin_id: user_plugin.id, + }; + + // Create a plugin config with user-specific credentials + let handle_config = self + .create_user_plugin_config(plugin_id, &user_plugin) + .await?; + + // Create handle with storage support for user plugins + let storage_handler = StorageRequestHandler::new(self.db.as_ref().clone(), user_plugin.id); + let handle = PluginHandle::new_with_storage(handle_config, storage_handler); + + // Start the plugin + match handle.start().await { + Ok(manifest) => { + // Record success for the user's instance + let _ = UserPluginsRepository::record_success(&self.db, user_plugin.id).await; + + // Persist manifest to DB so that userConfigSchema and + // capabilities are available on the UserPluginDto. + let manifest_json = serde_json::to_value(&manifest).unwrap_or_default(); + let _ = + PluginsRepository::update_manifest(&self.db, plugin_id, Some(manifest_json)) + .await; + + Ok((Arc::new(handle), context)) + } + Err(e) => { + // Record failure for the user's instance + let _ = UserPluginsRepository::record_failure(&self.db, user_plugin.id).await; + Err(PluginManagerError::Plugin(e)) + } + } } - /// Get all managed plugin configurations - pub async fn all_plugins(&self) -> Vec { - let plugins = self.plugins.read().await; - plugins.values().map(|e| e.db_config.clone()).collect() + /// Create a PluginConfig for a user plugin instance + /// + /// Uses the base plugin definition (command, args, etc.) but injects + /// per-user credentials from the user_plugins table. + async fn create_user_plugin_config( + &self, + plugin_id: Uuid, + user_plugin: &user_plugins::Model, + ) -> Result { + // Get the base plugin definition + let plugin = PluginsRepository::get_by_id(&self.db, plugin_id) + .await? + .ok_or(PluginManagerError::PluginNotFound(plugin_id))?; + + // Build process config from the base plugin + let mut process_config = PluginProcessConfig::new(&plugin.command); + process_config = process_config + .plugin_name(&plugin.name) + .args(plugin.args_vec()); + + // Add environment variables from the base plugin config + for (key, value) in plugin.env_vec() { + process_config = process_config.env(&key, &value); + } + + if let Some(wd) = &plugin.working_directory { + process_config = process_config.working_directory(wd); + } + + // Inject per-user credentials + // + // For user plugins we always deliver credentials via init_message + // (the JSON-RPC initialize params) because SDK-based plugins read + // from `params.credentials`. We also honour the legacy env-var path + // when credential_delivery is "env" or "both". + let mut credentials: Option = None; + let delivery = plugin.credential_delivery.as_str(); + + // Try OAuth tokens first, then fall back to simple credentials + if user_plugin.has_oauth_tokens() { + // Ensure the OAuth token is still valid, refreshing if needed + let access_token = self.ensure_fresh_oauth_token(&plugin, user_plugin).await?; + + let cred_value = serde_json::json!({ + "access_token": access_token, + }); + + if matches!(delivery, "env" | "both") { + process_config = process_config.env("ACCESS_TOKEN", &access_token); + } + + // Always include in init_message for user plugins + credentials = Some(SecretValue::new(cred_value)); + } else if user_plugin.has_credentials() { + // Decrypt simple credentials for the user + if let Ok(Some(decrypted)) = + UserPluginsRepository::get_credentials(&self.db, user_plugin.id).await + { + if matches!(delivery, "env" | "both") + && let Some(obj) = decrypted.as_object() + { + for (key, value) in obj { + if let Some(v) = value.as_str() { + process_config = process_config.env(key.to_uppercase(), v); + } + } + } + + // Always include in init_message for user plugins + credentials = Some(SecretValue::new(decrypted)); + } + } + + // Send admin config and user config separately + let admin_config = Some(plugin.config.clone()); + let user_config = if user_plugin.config.is_object() + && !user_plugin.config.as_object().is_none_or(|o| o.is_empty()) + { + Some(user_plugin.config.clone()) + } else { + None + }; + + Ok(PluginConfig { + process: process_config, + request_timeout: self.config.default_request_timeout, + shutdown_timeout: self.config.default_shutdown_timeout, + max_failures: self.config.failure_threshold, + admin_config, + user_config, + credentials, + }) + } + + /// Ensure the user's OAuth access token is fresh, refreshing it if needed. + /// + /// Returns the valid access token (either the existing one or a freshly refreshed one). + /// Returns an error if re-authentication is required or the refresh fails. + async fn ensure_fresh_oauth_token( + &self, + plugin: &plugins::Model, + user_plugin: &user_plugins::Model, + ) -> Result { + // Extract OAuth config from the plugin manifest + let oauth_config = Self::get_oauth_config(plugin); + let client_id = Self::get_oauth_client_id(plugin); + + // If OAuth config or client_id is missing, skip refresh and just return the + // current access token (some plugins use non-standard OAuth flows) + if let (Some(oauth_config), Some(client_id)) = (&oauth_config, &client_id) { + let client_secret = Self::get_oauth_client_secret(plugin); + + match token_refresh::ensure_valid_token( + &self.db, + user_plugin, + oauth_config, + client_id, + client_secret.as_deref(), + ) + .await + { + Ok(RefreshResult::Refreshed { access_token }) => { + info!( + user_plugin_id = %user_plugin.id, + "Using refreshed OAuth token" + ); + return Ok(access_token); + } + Ok(RefreshResult::StillValid) => { + // Fall through to return the existing token + } + Ok(RefreshResult::ReauthRequired) => { + return Err(PluginManagerError::ReauthRequired(user_plugin.id)); + } + Ok(RefreshResult::Failed(reason)) => { + return Err(PluginManagerError::TokenRefreshFailed(reason)); + } + Err(e) => { + return Err(PluginManagerError::TokenRefreshFailed(e.to_string())); + } + } + } + + // Token is still valid (or no OAuth config to check against) — decrypt and return + UserPluginsRepository::get_oauth_access_token(&self.db, user_plugin.id) + .await + .map_err(PluginManagerError::Database)? + .ok_or_else(|| { + PluginManagerError::TokenRefreshFailed( + "Failed to decrypt OAuth access token".to_string(), + ) + }) + } + + /// Extract OAuth config from a plugin's stored manifest + fn get_oauth_config(plugin: &plugins::Model) -> Option { + let manifest_json = plugin.manifest.as_ref()?; + let manifest: PluginManifest = serde_json::from_value(manifest_json.clone()).ok()?; + manifest.oauth + } + + /// Get the OAuth client_id for a plugin (config override > manifest default) + fn get_oauth_client_id(plugin: &plugins::Model) -> Option { + // Check plugin config for client_id override + if let Some(client_id) = plugin + .config + .get("oauth_client_id") + .and_then(|v| v.as_str()) + { + return Some(client_id.to_string()); + } + + // Fall back to manifest's default client_id + Self::get_oauth_config(plugin)?.client_id + } + + /// Get OAuth client_secret from plugin config + fn get_oauth_client_secret(plugin: &plugins::Model) -> Option { + plugin + .config + .get("oauth_client_secret") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) } /// Check rate limit for a plugin. Returns Ok(plugin_name) if allowed, Err if rate limited. @@ -1186,12 +1433,6 @@ impl PluginManager { } } - /// Check if health checks are running - pub async fn health_checks_running(&self) -> bool { - let handle = self.health_check_handle.read().await; - handle.as_ref().is_some_and(|h| !h.is_finished()) - } - /// Record a plugin failure and check if it should be auto-disabled /// /// This uses time-windowed failure tracking instead of consecutive failure counts. @@ -1318,7 +1559,8 @@ impl PluginManager { request_timeout: self.config.default_request_timeout, shutdown_timeout: self.config.default_shutdown_timeout, max_failures: self.config.failure_threshold, - config: Some(plugin.config.clone()), + admin_config: Some(plugin.config.clone()), + user_config: None, credentials, }) } @@ -1330,9 +1572,7 @@ impl PluginManager { PluginError::Rpc(_) => "RPC_ERROR", PluginError::NotInitialized => "NOT_INITIALIZED", PluginError::Disabled { .. } => "DISABLED", - PluginError::HealthCheckFailed(_) => "HEALTH_CHECK_FAILED", PluginError::SpawnFailed(_) => "SPAWN_FAILED", - PluginError::InvalidManifest(_) => "INVALID_MANIFEST", } } } @@ -1610,6 +1850,143 @@ mod tests { assert_eq!(entry.rate_limiter.as_ref().unwrap().capacity(), 60); } + fn create_test_plugin( + config: serde_json::Value, + manifest: Option, + ) -> plugins::Model { + use chrono::Utc; + + plugins::Model { + id: Uuid::new_v4(), + name: "test-plugin".to_string(), + display_name: "Test Plugin".to_string(), + description: None, + plugin_type: "user".to_string(), + command: "node".to_string(), + args: serde_json::json!([]), + env: serde_json::json!({}), + working_directory: None, + permissions: serde_json::json!([]), + scopes: serde_json::json!([]), + library_ids: serde_json::json!([]), + credentials: None, + credential_delivery: "init_message".to_string(), + config, + manifest, + enabled: true, + health_status: "healthy".to_string(), + failure_count: 0, + last_failure_at: None, + last_success_at: None, + disabled_reason: None, + rate_limit_requests_per_minute: None, + search_query_template: None, + search_preprocessing_rules: None, + auto_match_conditions: None, + use_existing_external_id: true, + metadata_targets: None, + created_at: Utc::now(), + updated_at: Utc::now(), + created_by: None, + updated_by: None, + } + } + + fn create_manifest_with_oauth(client_id: Option<&str>) -> serde_json::Value { + serde_json::json!({ + "name": "test-plugin", + "displayName": "Test Plugin", + "version": "1.0.0", + "description": "Test", + "protocolVersion": "1.0", + "capabilities": {}, + "oauth": { + "authorizationUrl": "https://example.com/auth", + "tokenUrl": "https://example.com/token", + "clientId": client_id, + "scopes": ["read"] + } + }) + } + + #[test] + fn test_get_oauth_config_from_manifest() { + let manifest = create_manifest_with_oauth(Some("manifest-client")); + let plugin = create_test_plugin(serde_json::json!({}), Some(manifest)); + + let oauth = PluginManager::get_oauth_config(&plugin); + assert!(oauth.is_some()); + + let oauth = oauth.unwrap(); + assert_eq!(oauth.token_url, "https://example.com/token"); + assert_eq!(oauth.authorization_url, "https://example.com/auth"); + assert_eq!(oauth.client_id.as_deref(), Some("manifest-client")); + } + + #[test] + fn test_get_oauth_config_no_manifest() { + let plugin = create_test_plugin(serde_json::json!({}), None); + assert!(PluginManager::get_oauth_config(&plugin).is_none()); + } + + #[test] + fn test_get_oauth_config_manifest_without_oauth() { + let manifest = serde_json::json!({ + "name": "test-plugin", + "displayName": "Test Plugin", + "version": "1.0.0", + "description": "Test", + "protocolVersion": "1.0", + "capabilities": {} + }); + let plugin = create_test_plugin(serde_json::json!({}), Some(manifest)); + assert!(PluginManager::get_oauth_config(&plugin).is_none()); + } + + #[test] + fn test_get_oauth_client_id_from_config_override() { + let manifest = create_manifest_with_oauth(Some("manifest-client")); + let plugin = create_test_plugin( + serde_json::json!({"oauth_client_id": "config-override"}), + Some(manifest), + ); + + let client_id = PluginManager::get_oauth_client_id(&plugin); + assert_eq!(client_id.as_deref(), Some("config-override")); + } + + #[test] + fn test_get_oauth_client_id_falls_back_to_manifest() { + let manifest = create_manifest_with_oauth(Some("manifest-client")); + let plugin = create_test_plugin(serde_json::json!({}), Some(manifest)); + + let client_id = PluginManager::get_oauth_client_id(&plugin); + assert_eq!(client_id.as_deref(), Some("manifest-client")); + } + + #[test] + fn test_get_oauth_client_id_none_when_no_config_or_manifest() { + let plugin = create_test_plugin(serde_json::json!({}), None); + assert!(PluginManager::get_oauth_client_id(&plugin).is_none()); + } + + #[test] + fn test_get_oauth_client_secret_from_config() { + let plugin = create_test_plugin( + serde_json::json!({"oauth_client_secret": "my-secret"}), + None, + ); + + let secret = PluginManager::get_oauth_client_secret(&plugin); + assert_eq!(secret.as_deref(), Some("my-secret")); + } + + #[test] + fn test_get_oauth_client_secret_none_when_missing() { + let plugin = create_test_plugin(serde_json::json!({}), None); + assert!(PluginManager::get_oauth_client_secret(&plugin).is_none()); + } + // Integration tests require a database connection // See tests/integration/plugin_manager.rs for full tests } diff --git a/src/services/plugin/mod.rs b/src/services/plugin/mod.rs index 3881e265..10835a1f 100644 --- a/src/services/plugin/mod.rs +++ b/src/services/plugin/mod.rs @@ -71,11 +71,16 @@ pub mod encryption; pub mod handle; pub mod health; +pub mod library; pub mod manager; pub mod process; pub mod protocol; +pub mod recommendations; pub mod rpc; pub mod secrets; +pub mod storage; +pub mod storage_handler; +pub mod sync; // Re-exports for public API // Note: Many of these are designed for future use or exposed for the complete API surface. @@ -84,7 +89,7 @@ pub mod secrets; #[allow(unused_imports)] pub use handle::PluginHandle; #[allow(unused_imports)] -pub use health::{HealthMonitor, HealthState, HealthTracker}; +pub use health::HealthTracker; #[allow(unused_imports)] pub use manager::{PluginManager, PluginManagerConfig, PluginManagerError}; #[allow(unused_imports)] diff --git a/src/services/plugin/process.rs b/src/services/plugin/process.rs index 90c6ea66..0d98654c 100644 --- a/src/services/plugin/process.rs +++ b/src/services/plugin/process.rs @@ -14,13 +14,8 @@ //! - `CODEX_PLUGIN_ALLOWED_COMMANDS` env var (comma-separated list) //! - Absolute paths starting with `/opt/codex/plugins/` are always allowed //! -//! Note: This module provides complete process management infrastructure. -//! Some methods and error variants may not be called from external code yet -//! but are part of the complete API for plugin process management. - -// Allow dead code for process management infrastructure that is part of the -// complete API surface but not yet fully integrated. -#![allow(dead_code)] +//! Note: Some builder methods and error variants are part of the complete API +//! surface but not yet called from external code. use std::collections::HashMap; use std::path::Path; @@ -338,6 +333,7 @@ impl PluginProcessConfig { } /// Add an argument + #[cfg(test)] pub fn arg(mut self, arg: impl Into) -> Self { self.args.push(arg.into()); self @@ -355,17 +351,6 @@ impl PluginProcessConfig { self } - /// Set multiple environment variables - pub fn envs( - mut self, - vars: impl IntoIterator, impl Into)>, - ) -> Self { - for (k, v) in vars { - self.env.insert(k.into(), v.into()); - } - self - } - /// Set the working directory pub fn working_directory(mut self, dir: impl Into) -> Self { self.working_directory = Some(dir.into()); @@ -397,12 +382,6 @@ pub enum ProcessError { #[error("Command '{command}' is not in the plugin allowlist. Allowed: {allowed}")] CommandNotAllowed { command: String, allowed: String }, - #[error("Plugin output line too long ({length} bytes, max {max} bytes)")] - LineTooLong { length: usize, max: usize }, - - #[error("Plugin output exceeded size limit ({total} bytes, max {max} bytes)")] - OutputTooLarge { total: usize, max: usize }, - #[error("Process stdin not available")] StdinUnavailable, @@ -412,18 +391,9 @@ pub enum ProcessError { #[error("Process stderr not available")] StderrUnavailable, - #[error("Failed to write to process stdin: {0}")] - WriteFailed(std::io::Error), - - #[error("Failed to read from process stdout: {0}")] - ReadFailed(std::io::Error), - #[error("Process terminated unexpectedly")] ProcessTerminated, - #[error("Process exited with code {0}")] - ExitCode(i32), - #[error("Channel closed")] ChannelClosed, } @@ -536,59 +506,6 @@ impl PluginProcess { .ok_or(ProcessError::ProcessTerminated) } - /// Check if the process is still running - pub fn is_running(&mut self) -> bool { - match self.child.try_wait() { - Ok(None) => true, // Still running - Ok(Some(status)) => { - // Process exited - log details for debugging - let pid = self.child.id(); - if let Some(code) = status.code() { - debug!( - pid = ?pid, - exit_code = code, - "Plugin process has exited with code" - ); - } else { - // On Unix, this means the process was killed by a signal - #[cfg(unix)] - { - use std::os::unix::process::ExitStatusExt; - if let Some(signal) = status.signal() { - warn!( - pid = ?pid, - signal = signal, - "Plugin process was killed by signal" - ); - } else { - debug!(pid = ?pid, "Plugin process exited without code or signal"); - } - } - #[cfg(not(unix))] - { - debug!(pid = ?pid, "Plugin process exited without code"); - } - } - false - } - Err(e) => { - warn!(error = %e, "Error checking plugin process status"); - false - } - } - } - - /// Get the process ID - pub fn pid(&self) -> Option { - self.child.id() - } - - /// Wait for the process to exit and return the exit code - pub async fn wait(&mut self) -> Result { - let status = self.child.wait().await?; - Ok(status.code().unwrap_or(-1)) - } - /// Kill the process pub async fn kill(&mut self) -> Result<(), ProcessError> { self.child.kill().await.map_err(ProcessError::SpawnFailed) @@ -1172,26 +1089,6 @@ mod tests { const _: () = assert!(MAX_TOTAL_OUTPUT > MAX_LINE_LENGTH); } - #[test] - fn test_error_variants() { - // Test that the error variants format correctly - let err = ProcessError::LineTooLong { - length: 2_000_000, - max: MAX_LINE_LENGTH, - }; - let msg = err.to_string(); - assert!(msg.contains("2000000")); - assert!(msg.contains("1048576")); - - let err = ProcessError::OutputTooLarge { - total: 200_000_000, - max: MAX_TOTAL_OUTPUT, - }; - let msg = err.to_string(); - assert!(msg.contains("200000000")); - assert!(msg.contains("104857600")); - } - // ========================================================================= // Environment Variable Blocklist Tests // ========================================================================= diff --git a/src/services/plugin/protocol.rs b/src/services/plugin/protocol.rs index d0c2b3ff..74543871 100644 --- a/src/services/plugin/protocol.rs +++ b/src/services/plugin/protocol.rs @@ -7,10 +7,6 @@ //! are designed for serialization/deserialization. They may not all be used internally //! yet, but form the complete API contract for plugin communication. -// Allow dead code for protocol types that are part of the API contract but not yet used internally. -// These types are essential for the complete plugin protocol specification. -#![allow(dead_code)] - use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -18,6 +14,7 @@ use serde_json::Value; pub const JSONRPC_VERSION: &str = "2.0"; /// Plugin protocol version +#[allow(dead_code)] // Protocol contract: sent to plugins during initialize pub const PROTOCOL_VERSION: &str = "1.0"; // ============================================================================= @@ -62,6 +59,7 @@ pub struct JsonRpcRequest { impl JsonRpcRequest { /// Create a new JSON-RPC request + #[allow(dead_code)] // Protocol contract: used by plugins and tests pub fn new(id: impl Into, method: impl Into, params: Option) -> Self { Self { jsonrpc: JSONRPC_VERSION.to_string(), @@ -72,6 +70,7 @@ impl JsonRpcRequest { } /// Create a request without parameters + #[allow(dead_code)] // Protocol contract: convenience constructor pub fn without_params(id: impl Into, method: impl Into) -> Self { Self::new(id, method, None) } @@ -111,6 +110,7 @@ impl JsonRpcResponse { } /// Check if the response is an error + #[allow(dead_code)] // Protocol contract: response inspection utility pub fn is_error(&self) -> bool { self.error.is_some() } @@ -134,6 +134,7 @@ impl JsonRpcError { } } + #[allow(dead_code)] // Protocol contract: error constructor with payload pub fn with_data(code: i32, message: impl Into, data: Value) -> Self { Self { code, @@ -150,8 +151,10 @@ impl JsonRpcError { /// Standard JSON-RPC error codes pub mod error_codes { /// Invalid JSON was received + #[allow(dead_code)] // Standard JSON-RPC error code pub const PARSE_ERROR: i32 = -32700; /// The JSON sent is not a valid Request object + #[allow(dead_code)] // Standard JSON-RPC error code pub const INVALID_REQUEST: i32 = -32600; /// The method does not exist / is not available pub const METHOD_NOT_FOUND: i32 = -32601; @@ -202,6 +205,39 @@ pub mod methods { pub const METADATA_BOOK_GET: &str = "metadata/book/get"; /// Find best match for a book (ISBN first, then title fallback) pub const METADATA_BOOK_MATCH: &str = "metadata/book/match"; + + // Storage methods (user plugin data) + /// Get a value by key from plugin storage + pub const STORAGE_GET: &str = "storage/get"; + /// Set a value by key in plugin storage (upsert) + pub const STORAGE_SET: &str = "storage/set"; + /// Delete a value by key from plugin storage + pub const STORAGE_DELETE: &str = "storage/delete"; + /// List all keys in plugin storage + pub const STORAGE_LIST: &str = "storage/list"; + /// Clear all data from plugin storage + pub const STORAGE_CLEAR: &str = "storage/clear"; + + // Sync methods (user plugin sync providers) + /// Get user info from external service + pub const SYNC_GET_USER_INFO: &str = "sync/getUserInfo"; + /// Push reading progress to external service + pub const SYNC_PUSH_PROGRESS: &str = "sync/pushProgress"; + /// Pull reading progress from external service + pub const SYNC_PULL_PROGRESS: &str = "sync/pullProgress"; + /// Get sync status/diff between Codex and external service + pub const SYNC_STATUS: &str = "sync/status"; + + // Recommendation methods (user plugin recommendation providers) + /// Get personalized recommendations + pub const RECOMMENDATIONS_GET: &str = "recommendations/get"; + /// Update taste profile from new user activity + #[allow(dead_code)] // Protocol contract: method available for future use + pub const RECOMMENDATIONS_UPDATE_PROFILE: &str = "recommendations/updateProfile"; + /// Clear cached recommendations + pub const RECOMMENDATIONS_CLEAR: &str = "recommendations/clear"; + /// Dismiss a recommendation (user not interested) + pub const RECOMMENDATIONS_DISMISS: &str = "recommendations/dismiss"; } // ============================================================================= @@ -238,9 +274,33 @@ pub struct PluginManifest { #[serde(default)] pub required_credentials: Vec, - /// JSON Schema for plugin-specific configuration + /// JSON Schema for plugin-specific configuration (admin-facing) #[serde(default, skip_serializing_if = "Option::is_none")] pub config_schema: Option, + + /// Configuration schema for per-user settings (user-facing) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_config_schema: Option, + + /// Plugin type: "system" (admin-only metadata) or "user" (per-user integrations) + #[serde(default)] + pub plugin_type: PluginManifestType, + + /// OAuth 2.0 configuration for user plugins that require external service authentication + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oauth: Option, + + /// User-facing description shown when enabling the plugin + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_description: Option, + + /// Admin-facing setup instructions (e.g., how to create OAuth app, set client ID) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub admin_setup_instructions: Option, + + /// User-facing setup instructions (e.g., how to connect or get a personal token) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_setup_instructions: Option, } /// Content types that a metadata provider can support @@ -263,7 +323,17 @@ pub struct PluginCapabilities { pub metadata_provider: Vec, /// Can sync user reading progress (v2) #[serde(default)] - pub user_sync_provider: bool, + pub user_read_sync: bool, + /// External ID source used to match sync entries to Codex series. + /// When set, pulled sync entries are matched to series via the + /// `series_external_ids` table using this source string. + /// Uses the `api:` convention, e.g. "api:anilist". + /// Only meaningful when `user_read_sync` is true. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_id_source: Option, + /// Can provide personalized recommendations (v2) + #[serde(default)] + pub user_recommendation_provider: bool, } impl PluginCapabilities { @@ -277,6 +347,110 @@ impl PluginCapabilities { pub fn can_provide_book_metadata(&self) -> bool { self.metadata_provider.contains(&MetadataContentType::Book) } + + /// Infer the plugin type from capabilities. + /// + /// User-facing capabilities (`user_read_sync`, `user_recommendation_provider`) + /// indicate a "user" plugin. Metadata provider capabilities indicate a + /// "system" plugin. Returns `None` when capabilities are empty. + pub fn inferred_plugin_type(&self) -> Option { + if self.user_read_sync || self.user_recommendation_provider { + Some(PluginManifestType::User) + } else if !self.metadata_provider.is_empty() { + Some(PluginManifestType::System) + } else { + None + } + } +} + +/// Plugin manifest type (declared by the plugin in its manifest) +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PluginManifestType { + /// System plugin: admin-configured, operates on shared library metadata + #[default] + System, + /// User plugin: per-user integrations (sync, recommendations) + User, +} + +impl std::fmt::Display for PluginManifestType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::System => write!(f, "system"), + Self::User => write!(f, "user"), + } + } +} + +/// OAuth 2.0 configuration for user plugins +/// +/// Plugins declare their OAuth requirements in the manifest. Codex handles +/// the OAuth flow (authorization URL generation, code exchange, token storage) +/// so plugins never directly interact with the OAuth provider. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OAuthConfig { + /// OAuth 2.0 authorization endpoint URL + pub authorization_url: String, + /// OAuth 2.0 token endpoint URL + pub token_url: String, + /// Required OAuth scopes + #[serde(default)] + pub scopes: Vec, + /// Whether to use PKCE (Proof Key for Code Exchange) + /// Recommended for public clients; defaults to true + #[serde(default = "default_true")] + pub pkce: bool, + /// Optional user info endpoint URL (to fetch external identity after auth) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_info_url: Option, + /// OAuth client ID (can be overridden by admin in plugin config) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_id: Option, +} + +fn default_true() -> bool { + true +} + +impl OAuthConfig { + /// Validate that the OAuth config has all required fields + #[allow(dead_code)] // Protocol contract: validation for plugin registration + pub fn validate(&self) -> Result<(), String> { + if self.authorization_url.is_empty() { + return Err("OAuth authorization_url is required".to_string()); + } + if self.token_url.is_empty() { + return Err("OAuth token_url is required".to_string()); + } + // Validate URLs start with https:// (or http:// for local dev) + if !self.authorization_url.starts_with("https://") + && !self.authorization_url.starts_with("http://") + { + return Err(format!( + "Invalid OAuth authorization_url (must start with http:// or https://): {}", + self.authorization_url + )); + } + if !self.token_url.starts_with("https://") && !self.token_url.starts_with("http://") { + return Err(format!( + "Invalid OAuth token_url (must start with http:// or https://): {}", + self.token_url + )); + } + if let Some(ref user_info_url) = self.user_info_url + && !user_info_url.starts_with("https://") + && !user_info_url.starts_with("http://") + { + return Err(format!( + "Invalid OAuth user_info_url (must start with http:// or https://): {}", + user_info_url + )); + } + Ok(()) + } } /// Credential field definition @@ -366,6 +540,7 @@ impl PluginScope { } /// Get scopes available for book metadata providers + #[allow(dead_code)] // Protocol contract: scope helpers for book metadata plugins pub fn book_scopes() -> Vec { vec![ Self::BookDetail, @@ -376,6 +551,7 @@ impl PluginScope { } /// Get all scopes (series + book + library) + #[allow(dead_code)] // Protocol contract: scope helpers for multi-content plugins pub fn all_scopes() -> Vec { vec![ Self::SeriesDetail, @@ -519,11 +695,13 @@ pub struct BookSearchParams { impl BookSearchParams { /// Check if this is an ISBN search + #[allow(dead_code)] // Protocol contract: query type inspection pub fn is_isbn_search(&self) -> bool { self.isbn.is_some() } /// Check if this is a query-based search + #[allow(dead_code)] // Protocol contract: query type inspection pub fn is_query_search(&self) -> bool { self.query.is_some() } @@ -619,6 +797,12 @@ pub struct PluginSeriesMetadata { // External links #[serde(default)] pub external_links: Vec, + + // External IDs (cross-references to other services) + /// Cross-reference IDs from other services (e.g., AniList, MAL, MangaDex). + /// These use the `api:` prefix convention (e.g., "api:anilist"). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub external_ids: Vec, } /// Full book metadata from a provider @@ -870,6 +1054,25 @@ pub struct ExternalRating { pub source: String, } +/// External ID cross-reference from a metadata provider +/// +/// Allows metadata plugins to return IDs for the same series on other services. +/// For example, a MangaBaka plugin can return the AniList and MAL IDs it knows about. +/// +/// ## Source Naming Convention +/// +/// - `api:` - External API service ID (e.g., "api:anilist", "api:myanimelist") +/// - `plugin:` - Plugin match provenance (managed by Codex, not returned by plugins) +/// - No prefix - File/user sources (e.g., "comicinfo", "epub", "manual") +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginExternalId { + /// Source identifier (e.g., "api:anilist", "api:myanimelist", "api:mangadex") + pub source: String, + /// ID on the external service + pub external_id: String, +} + /// External link to other sites #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -892,6 +1095,97 @@ pub enum ExternalLinkType { Other, } +// ============================================================================= +// User Library Data Contract (Sync Providers) +// ============================================================================= + +/// A user's library entry sent to sync plugins +/// +/// Contains series info, reading progress, and the user's personal data +/// (rating, notes) needed for sync providers to push/pull state with +/// external services. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserLibraryEntry { + /// Codex series ID + pub series_id: String, + /// Primary title + pub title: String, + /// Alternative titles (native, romaji, etc.) + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub alternate_titles: Vec, + /// Publication year + #[serde(default, skip_serializing_if = "Option::is_none")] + pub year: Option, + /// Series status + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, + /// Genres + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub genres: Vec, + /// Tags + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + /// Total book count in the series + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_book_count: Option, + + /// Known external IDs (source → external_id mapping) + /// e.g., {"anilist": "12345", "myanimelist": "67890"} + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub external_ids: Vec, + + /// User's reading status (derived from progress across books) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reading_status: Option, + /// Number of books the user has completed in this series + #[serde(default)] + pub books_read: i32, + /// Total number of books in the user's library for this series + #[serde(default)] + pub books_owned: i32, + /// User's personal rating (1-100 scale) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_rating: Option, + /// User's personal notes + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_notes: Option, + /// When the user started reading (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + /// When the user last read (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_read_at: Option, + /// When the user completed the series (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub completed_at: Option, +} + +/// External ID mapping for a library entry +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserLibraryExternalId { + /// Source name (e.g., "anilist", "myanimelist", "mangadex") + pub source: String, + /// External ID on that service + pub external_id: String, + /// URL to the entry on the external service + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_url: Option, +} + +/// User's reading status for a series (derived from book progress) +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UserReadingStatus { + /// User has not started reading + Unread, + /// User is currently reading (some books have progress) + Reading, + /// User has completed all available books + Completed, +} + // ============================================================================= // Initialize Response // ============================================================================= @@ -900,9 +1194,15 @@ pub enum ExternalLinkType { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InitializeParams { - /// Plugin configuration from Codex + /// Plugin configuration from Codex (merged admin + user, deprecated) #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, + /// Admin-level plugin configuration (from plugin settings) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub admin_config: Option, + /// Per-user plugin configuration (from user plugin settings) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_config: Option, /// Credentials passed via init message (alternative to env vars) #[serde(default, skip_serializing_if = "Option::is_none")] pub credentials: Option, @@ -913,6 +1213,7 @@ pub struct InitializeParams { // ============================================================================= /// Data included with rate limit errors +#[allow(dead_code)] // Protocol contract: rate limit error payload schema #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RateLimitErrorData { @@ -1112,11 +1413,80 @@ mod tests { }), external_ratings: vec![], external_links: vec![], + external_ids: vec![ + PluginExternalId { + source: "api:anilist".to_string(), + external_id: "21".to_string(), + }, + PluginExternalId { + source: "api:myanimelist".to_string(), + external_id: "13".to_string(), + }, + ], }; let json = serde_json::to_value(&metadata).unwrap(); assert_eq!(json["externalId"], "12345"); assert_eq!(json["status"], "ongoing"); + let ext_ids = json["externalIds"].as_array().unwrap(); + assert_eq!(ext_ids.len(), 2); + assert_eq!(ext_ids[0]["source"], "api:anilist"); + assert_eq!(ext_ids[0]["externalId"], "21"); + assert_eq!(ext_ids[1]["source"], "api:myanimelist"); + assert_eq!(ext_ids[1]["externalId"], "13"); + } + + #[test] + fn test_plugin_external_id_serialization() { + let ext_id = PluginExternalId { + source: "api:anilist".to_string(), + external_id: "97".to_string(), + }; + let json = serde_json::to_value(&ext_id).unwrap(); + assert_eq!(json["source"], "api:anilist"); + assert_eq!(json["externalId"], "97"); + } + + #[test] + fn test_plugin_external_id_deserialization() { + let json = serde_json::json!({ + "source": "api:mangadex", + "externalId": "abc-def-123" + }); + let ext_id: PluginExternalId = serde_json::from_value(json).unwrap(); + assert_eq!(ext_id.source, "api:mangadex"); + assert_eq!(ext_id.external_id, "abc-def-123"); + } + + #[test] + fn test_plugin_series_metadata_empty_external_ids_skipped() { + let metadata = PluginSeriesMetadata { + external_id: "1".to_string(), + external_url: "https://example.com/1".to_string(), + title: None, + alternate_titles: vec![], + summary: None, + status: None, + year: None, + total_book_count: None, + language: None, + age_rating: None, + reading_direction: None, + genres: vec![], + tags: vec![], + authors: vec![], + artists: vec![], + publisher: None, + cover_url: None, + banner_url: None, + rating: None, + external_ratings: vec![], + external_links: vec![], + external_ids: vec![], + }; + let json = serde_json::to_value(&metadata).unwrap(); + // externalIds should be omitted when empty + assert!(!json.as_object().unwrap().contains_key("externalIds")); } #[test] @@ -1363,4 +1733,529 @@ mod tests { assert!(scopes.contains(&PluginScope::BookDetail)); assert_eq!(scopes.len(), 6); } + + // ========================================================================= + // OAuth Config & User Plugin Tests + // ========================================================================= + + #[test] + fn test_plugin_manifest_type_default() { + let manifest_type: PluginManifestType = Default::default(); + assert_eq!(manifest_type, PluginManifestType::System); + } + + #[test] + fn test_plugin_manifest_type_serialization() { + let system = PluginManifestType::System; + let user = PluginManifestType::User; + assert_eq!(serde_json::to_value(&system).unwrap(), json!("system")); + assert_eq!(serde_json::to_value(&user).unwrap(), json!("user")); + } + + #[test] + fn test_plugin_manifest_type_deserialization() { + let system: PluginManifestType = serde_json::from_value(json!("system")).unwrap(); + let user: PluginManifestType = serde_json::from_value(json!("user")).unwrap(); + assert_eq!(system, PluginManifestType::System); + assert_eq!(user, PluginManifestType::User); + } + + #[test] + fn test_plugin_manifest_type_display() { + assert_eq!(PluginManifestType::System.to_string(), "system"); + assert_eq!(PluginManifestType::User.to_string(), "user"); + } + + #[test] + fn test_inferred_plugin_type_from_user_read_sync() { + let caps = PluginCapabilities { + user_read_sync: true, + ..Default::default() + }; + assert_eq!(caps.inferred_plugin_type(), Some(PluginManifestType::User)); + } + + #[test] + fn test_inferred_plugin_type_from_recommendation_provider() { + let caps = PluginCapabilities { + user_recommendation_provider: true, + ..Default::default() + }; + assert_eq!(caps.inferred_plugin_type(), Some(PluginManifestType::User)); + } + + #[test] + fn test_inferred_plugin_type_from_metadata_provider() { + let caps = PluginCapabilities { + metadata_provider: vec![MetadataContentType::Series], + ..Default::default() + }; + assert_eq!( + caps.inferred_plugin_type(), + Some(PluginManifestType::System) + ); + } + + #[test] + fn test_inferred_plugin_type_empty_capabilities() { + let caps = PluginCapabilities::default(); + assert_eq!(caps.inferred_plugin_type(), None); + } + + #[test] + fn test_oauth_config_serialization() { + let config = OAuthConfig { + authorization_url: "https://anilist.co/api/v2/oauth/authorize".to_string(), + token_url: "https://anilist.co/api/v2/oauth/token".to_string(), + scopes: vec!["read".to_string(), "write".to_string()], + pkce: true, + user_info_url: Some("https://graphql.anilist.co".to_string()), + client_id: None, + }; + + let json = serde_json::to_value(&config).unwrap(); + assert_eq!( + json["authorizationUrl"], + "https://anilist.co/api/v2/oauth/authorize" + ); + assert_eq!(json["tokenUrl"], "https://anilist.co/api/v2/oauth/token"); + assert_eq!(json["scopes"], json!(["read", "write"])); + assert!(json["pkce"].as_bool().unwrap()); + assert_eq!(json["userInfoUrl"], "https://graphql.anilist.co"); + } + + #[test] + fn test_oauth_config_deserialization() { + let json = json!({ + "authorizationUrl": "https://myanimelist.net/v1/oauth2/authorize", + "tokenUrl": "https://myanimelist.net/v1/oauth2/token", + "scopes": ["read"], + "pkce": true + }); + + let config: OAuthConfig = serde_json::from_value(json).unwrap(); + assert_eq!( + config.authorization_url, + "https://myanimelist.net/v1/oauth2/authorize" + ); + assert_eq!(config.token_url, "https://myanimelist.net/v1/oauth2/token"); + assert_eq!(config.scopes, vec!["read"]); + assert!(config.pkce); + assert!(config.user_info_url.is_none()); + } + + #[test] + fn test_oauth_config_pkce_defaults_to_true() { + let json = json!({ + "authorizationUrl": "https://example.com/auth", + "tokenUrl": "https://example.com/token" + }); + + let config: OAuthConfig = serde_json::from_value(json).unwrap(); + assert!(config.pkce); + } + + #[test] + fn test_oauth_config_validate_valid() { + let config = OAuthConfig { + authorization_url: "https://example.com/auth".to_string(), + token_url: "https://example.com/token".to_string(), + scopes: vec![], + pkce: true, + user_info_url: None, + client_id: None, + }; + assert!(config.validate().is_ok()); + } + + #[test] + fn test_oauth_config_validate_empty_auth_url() { + let config = OAuthConfig { + authorization_url: "".to_string(), + token_url: "https://example.com/token".to_string(), + scopes: vec![], + pkce: true, + user_info_url: None, + client_id: None, + }; + assert!(config.validate().is_err()); + } + + #[test] + fn test_oauth_config_validate_invalid_url() { + let config = OAuthConfig { + authorization_url: "not-a-url".to_string(), + token_url: "https://example.com/token".to_string(), + scopes: vec![], + pkce: true, + user_info_url: None, + client_id: None, + }; + assert!(config.validate().is_err()); + } + + #[test] + fn test_oauth_config_validate_with_user_info_url() { + let config = OAuthConfig { + authorization_url: "https://example.com/auth".to_string(), + token_url: "https://example.com/token".to_string(), + scopes: vec![], + pkce: true, + user_info_url: Some("https://example.com/userinfo".to_string()), + client_id: None, + }; + assert!(config.validate().is_ok()); + } + + #[test] + fn test_oauth_config_validate_invalid_user_info_url() { + let config = OAuthConfig { + authorization_url: "https://example.com/auth".to_string(), + token_url: "https://example.com/token".to_string(), + scopes: vec![], + pkce: true, + user_info_url: Some("not-a-url".to_string()), + client_id: None, + }; + assert!(config.validate().is_err()); + } + + #[test] + fn test_plugin_manifest_with_oauth_config() { + let json = json!({ + "name": "sync-anilist", + "displayName": "AniList Sync", + "version": "1.0.0", + "protocolVersion": "1.0", + "pluginType": "user", + "capabilities": { + "userReadSync": true + }, + "oauth": { + "authorizationUrl": "https://anilist.co/api/v2/oauth/authorize", + "tokenUrl": "https://anilist.co/api/v2/oauth/token", + "scopes": [], + "pkce": false + }, + "userDescription": "Sync reading progress with AniList", + "adminSetupInstructions": "Create an AniList app at ...", + "userSetupInstructions": "Click Connect to link your AniList account" + }); + + let manifest: PluginManifest = serde_json::from_value(json).unwrap(); + assert_eq!(manifest.name, "sync-anilist"); + assert_eq!(manifest.plugin_type, PluginManifestType::User); + assert!(manifest.capabilities.user_read_sync); + assert!(!manifest.capabilities.user_recommendation_provider); + + let oauth = manifest.oauth.unwrap(); + assert_eq!( + oauth.authorization_url, + "https://anilist.co/api/v2/oauth/authorize" + ); + assert!(!oauth.pkce); + + assert_eq!( + manifest.user_description.unwrap(), + "Sync reading progress with AniList" + ); + assert!(manifest.admin_setup_instructions.is_some()); + assert!(manifest.user_setup_instructions.is_some()); + } + + #[test] + fn test_plugin_manifest_defaults_to_system_type() { + let json = json!({ + "name": "metadata-plugin", + "displayName": "Metadata Plugin", + "version": "1.0.0", + "protocolVersion": "1.0", + "capabilities": { + "metadataProvider": ["series"] + } + }); + + let manifest: PluginManifest = serde_json::from_value(json).unwrap(); + assert_eq!(manifest.plugin_type, PluginManifestType::System); + assert!(manifest.oauth.is_none()); + assert!(manifest.user_description.is_none()); + } + + #[test] + fn test_plugin_capabilities_recommendation_provider() { + let json = json!({ + "name": "rec-engine", + "displayName": "Recommendation Engine", + "version": "1.0.0", + "protocolVersion": "1.0", + "pluginType": "user", + "capabilities": { + "userRecommendationProvider": true + } + }); + + let manifest: PluginManifest = serde_json::from_value(json).unwrap(); + assert!(manifest.capabilities.user_recommendation_provider); + assert!(!manifest.capabilities.user_read_sync); + assert!(manifest.capabilities.metadata_provider.is_empty()); + } + + // ========================================================================= + // User Library Data Contract Tests + // ========================================================================= + + #[test] + fn test_user_library_entry_full_serialization() { + let entry = UserLibraryEntry { + series_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), + title: "One Piece".to_string(), + alternate_titles: vec!["ワンピース".to_string()], + year: Some(1997), + status: Some(SeriesStatus::Ongoing), + genres: vec!["Action".to_string(), "Adventure".to_string()], + tags: vec!["pirates".to_string()], + total_book_count: Some(107), + external_ids: vec![UserLibraryExternalId { + source: "anilist".to_string(), + external_id: "21".to_string(), + external_url: Some("https://anilist.co/manga/21".to_string()), + }], + reading_status: Some(UserReadingStatus::Reading), + books_read: 95, + books_owned: 100, + user_rating: Some(95), + user_notes: Some("Masterpiece".to_string()), + started_at: Some("2024-01-01T00:00:00Z".to_string()), + last_read_at: Some("2026-02-06T00:00:00Z".to_string()), + completed_at: None, + }; + let json = serde_json::to_value(&entry).unwrap(); + assert_eq!(json["seriesId"], "550e8400-e29b-41d4-a716-446655440000"); + assert_eq!(json["title"], "One Piece"); + assert_eq!(json["alternateTitles"][0], "ワンピース"); + assert_eq!(json["year"], 1997); + assert_eq!(json["status"], "ongoing"); + assert_eq!(json["genres"].as_array().unwrap().len(), 2); + assert_eq!(json["totalBookCount"], 107); + assert_eq!(json["externalIds"][0]["source"], "anilist"); + assert_eq!(json["externalIds"][0]["externalId"], "21"); + assert_eq!(json["readingStatus"], "reading"); + assert_eq!(json["booksRead"], 95); + assert_eq!(json["booksOwned"], 100); + assert_eq!(json["userRating"], 95); + assert_eq!(json["userNotes"], "Masterpiece"); + assert!(!json.as_object().unwrap().contains_key("completedAt")); + } + + #[test] + fn test_user_library_entry_minimal() { + let entry = UserLibraryEntry { + series_id: "abc".to_string(), + title: "Test".to_string(), + alternate_titles: vec![], + year: None, + status: None, + genres: vec![], + tags: vec![], + total_book_count: None, + external_ids: vec![], + reading_status: None, + books_read: 0, + books_owned: 3, + user_rating: None, + user_notes: None, + started_at: None, + last_read_at: None, + completed_at: None, + }; + let json = serde_json::to_value(&entry).unwrap(); + assert_eq!(json["seriesId"], "abc"); + assert_eq!(json["title"], "Test"); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("alternateTitles")); + assert!(!obj.contains_key("year")); + assert!(!obj.contains_key("status")); + assert!(!obj.contains_key("genres")); + assert!(!obj.contains_key("externalIds")); + assert!(!obj.contains_key("readingStatus")); + assert!(!obj.contains_key("userRating")); + } + + #[test] + fn test_user_library_entry_deserialization() { + let json = json!({ + "seriesId": "123", + "title": "Berserk", + "readingStatus": "completed", + "booksRead": 42, + "booksOwned": 42, + "userRating": 100, + "completedAt": "2025-12-01T00:00:00Z" + }); + let entry: UserLibraryEntry = serde_json::from_value(json).unwrap(); + assert_eq!(entry.series_id, "123"); + assert_eq!(entry.title, "Berserk"); + assert_eq!(entry.reading_status, Some(UserReadingStatus::Completed)); + assert_eq!(entry.books_read, 42); + assert_eq!(entry.user_rating, Some(100)); + assert_eq!(entry.completed_at.unwrap(), "2025-12-01T00:00:00Z"); + } + + #[test] + fn test_user_library_external_id_serialization() { + let ext_id = UserLibraryExternalId { + source: "myanimelist".to_string(), + external_id: "99999".to_string(), + external_url: Some("https://myanimelist.net/manga/99999".to_string()), + }; + let json = serde_json::to_value(&ext_id).unwrap(); + assert_eq!(json["source"], "myanimelist"); + assert_eq!(json["externalId"], "99999"); + assert_eq!(json["externalUrl"], "https://myanimelist.net/manga/99999"); + } + + #[test] + fn test_user_library_external_id_without_url() { + let ext_id = UserLibraryExternalId { + source: "comicinfo".to_string(), + external_id: "abc".to_string(), + external_url: None, + }; + let json = serde_json::to_value(&ext_id).unwrap(); + assert!(!json.as_object().unwrap().contains_key("externalUrl")); + } + + #[test] + fn test_user_reading_status_serialization() { + assert_eq!( + serde_json::to_value(UserReadingStatus::Unread).unwrap(), + json!("unread") + ); + assert_eq!( + serde_json::to_value(UserReadingStatus::Reading).unwrap(), + json!("reading") + ); + assert_eq!( + serde_json::to_value(UserReadingStatus::Completed).unwrap(), + json!("completed") + ); + } + + #[test] + fn test_user_reading_status_deserialization() { + let unread: UserReadingStatus = serde_json::from_value(json!("unread")).unwrap(); + assert_eq!(unread, UserReadingStatus::Unread); + let reading: UserReadingStatus = serde_json::from_value(json!("reading")).unwrap(); + assert_eq!(reading, UserReadingStatus::Reading); + let completed: UserReadingStatus = serde_json::from_value(json!("completed")).unwrap(); + assert_eq!(completed, UserReadingStatus::Completed); + } + + #[test] + fn test_user_library_entry_multiple_external_ids() { + let entry = UserLibraryEntry { + series_id: "s1".to_string(), + title: "Test Series".to_string(), + alternate_titles: vec![], + year: None, + status: None, + genres: vec![], + tags: vec![], + total_book_count: None, + external_ids: vec![ + UserLibraryExternalId { + source: "anilist".to_string(), + external_id: "21".to_string(), + external_url: None, + }, + UserLibraryExternalId { + source: "myanimelist".to_string(), + external_id: "13".to_string(), + external_url: None, + }, + ], + reading_status: None, + books_read: 0, + books_owned: 0, + user_rating: None, + user_notes: None, + started_at: None, + last_read_at: None, + completed_at: None, + }; + let json = serde_json::to_value(&entry).unwrap(); + let ids = json["externalIds"].as_array().unwrap(); + assert_eq!(ids.len(), 2); + assert_eq!(ids[0]["source"], "anilist"); + assert_eq!(ids[1]["source"], "myanimelist"); + } + + // ========================================================================= + // InitializeParams Tests + // ========================================================================= + + #[test] + fn test_initialize_params_with_split_config() { + let params = InitializeParams { + config: None, + admin_config: Some(json!({"clientId": "abc"})), + user_config: Some(json!({"progressUnit": "chapters"})), + credentials: Some(json!({"access_token": "secret"})), + }; + let json = serde_json::to_value(¶ms).unwrap(); + assert_eq!(json["adminConfig"]["clientId"], "abc"); + assert_eq!(json["userConfig"]["progressUnit"], "chapters"); + assert_eq!(json["credentials"]["access_token"], "secret"); + assert!(!json.as_object().unwrap().contains_key("config")); + } + + #[test] + fn test_initialize_params_with_legacy_config() { + let params = InitializeParams { + config: Some(json!({"merged": true})), + admin_config: None, + user_config: None, + credentials: None, + }; + let json = serde_json::to_value(¶ms).unwrap(); + assert_eq!(json["config"]["merged"], true); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("adminConfig")); + assert!(!obj.contains_key("userConfig")); + assert!(!obj.contains_key("credentials")); + } + + #[test] + fn test_initialize_params_deserialization_with_split_config() { + let json = json!({ + "adminConfig": {"clientId": "abc"}, + "userConfig": {"progressUnit": "chapters"}, + "credentials": {"access_token": "secret"} + }); + let params: InitializeParams = serde_json::from_value(json).unwrap(); + assert!(params.config.is_none()); + assert_eq!(params.admin_config.unwrap()["clientId"], "abc"); + assert_eq!(params.user_config.unwrap()["progressUnit"], "chapters"); + assert_eq!(params.credentials.unwrap()["access_token"], "secret"); + } + + #[test] + fn test_initialize_params_deserialization_backward_compat() { + // Old format: only config field (no adminConfig/userConfig) + let json = json!({ + "config": {"clientId": "abc", "progressUnit": "chapters"}, + "credentials": {"access_token": "secret"} + }); + let params: InitializeParams = serde_json::from_value(json).unwrap(); + assert_eq!(params.config.unwrap()["clientId"], "abc"); + assert!(params.admin_config.is_none()); + assert!(params.user_config.is_none()); + } + + #[test] + fn test_initialize_params_empty() { + let params = InitializeParams::default(); + let json = serde_json::to_value(¶ms).unwrap(); + assert_eq!(json, json!({})); + } } diff --git a/src/services/plugin/recommendations.rs b/src/services/plugin/recommendations.rs new file mode 100644 index 00000000..3fada87f --- /dev/null +++ b/src/services/plugin/recommendations.rs @@ -0,0 +1,561 @@ +//! Recommendation Provider Protocol Types +//! +//! Defines the JSON-RPC request/response types for recommendation provider operations. +//! Recommendation providers generate personalized suggestions based on the user's +//! library, ratings, and reading history. +//! +//! ## Architecture +//! +//! Recommendation operations are initiated by the host (Codex) and sent to the plugin. +//! The plugin analyzes the user's library data and returns recommendations, optionally +//! using external APIs (e.g., AniList recommendations) and caching results via the +//! storage system. +//! +//! ## Methods +//! +//! - `recommendations/get` - Get personalized recommendations +//! - `recommendations/updateProfile` - Update taste profile from new activity +//! - `recommendations/clear` - Clear cached recommendations + +use serde::{Deserialize, Serialize}; + +use super::protocol::UserLibraryEntry; + +// ============================================================================= +// Recommendation Request +// ============================================================================= + +/// Parameters for `recommendations/get` method +/// +/// Sends the user's library data to the plugin so it can generate +/// personalized recommendations. The plugin may use external APIs, +/// cached taste profiles, or both. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationRequest { + /// User's library entries (series with ratings, progress, etc.) + pub library: Vec, + /// Maximum number of recommendations to return + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// External IDs to exclude (e.g., series the user already has) + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub exclude_ids: Vec, +} + +// ============================================================================= +// Recommendation Response +// ============================================================================= + +/// Response from `recommendations/get` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationResponse { + /// List of personalized recommendations + pub recommendations: Vec, + /// When this set of recommendations was generated (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub generated_at: Option, + /// Whether these are cached results or freshly generated + #[serde(default)] + pub cached: bool, +} + +/// A single recommendation from the plugin +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Recommendation { + /// External ID on the source service (e.g., AniList media ID) + pub external_id: String, + /// URL to the entry on the external service + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_url: Option, + + /// Title of the recommended series/book + pub title: String, + /// Cover image URL + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cover_url: Option, + /// Summary/description + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// Genres + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub genres: Vec, + + /// Confidence/relevance score (0.0 to 1.0) + pub score: f64, + /// Human-readable reason for this recommendation + /// e.g., "Because you rated Berserk 10/10" + pub reason: String, + /// Titles that influenced this recommendation + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub based_on: Vec, + + /// Codex series ID if matched to an existing series in the library + #[serde(default, skip_serializing_if = "Option::is_none")] + pub codex_series_id: Option, + /// Whether this series is already in the user's library + #[serde(default)] + pub in_library: bool, +} + +// ============================================================================= +// Profile Update +// ============================================================================= + +/// Parameters for `recommendations/updateProfile` method +/// +/// Notifies the plugin of new user activity so it can update the +/// taste profile used for generating recommendations. +#[allow(dead_code)] // Protocol contract: updateProfile method not yet invoked by host +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProfileUpdateRequest { + /// Updated library entries (may be partial - only changed entries) + pub entries: Vec, +} + +/// Response from `recommendations/updateProfile` method +#[allow(dead_code)] // Protocol contract: updateProfile method not yet invoked by host +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProfileUpdateResponse { + /// Whether the profile was successfully updated + pub updated: bool, + /// Number of entries processed + #[serde(default)] + pub entries_processed: u32, +} + +// ============================================================================= +// Clear Recommendations +// ============================================================================= + +/// Response from `recommendations/clear` method +/// +/// Clears cached recommendations, forcing a fresh generation on next request. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationClearResponse { + /// Whether the clear was successful + pub cleared: bool, +} + +// ============================================================================= +// Dismiss Recommendation +// ============================================================================= + +/// Parameters for `recommendations/dismiss` method +/// +/// Tells the plugin that the user is not interested in a recommendation, +/// so it can be excluded from future results. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationDismissRequest { + /// External ID of the recommendation to dismiss + pub external_id: String, + /// Reason for dismissal (optional, may help improve future recommendations) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// Reason for dismissing a recommendation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DismissReason { + /// User is not interested + NotInterested, + /// User has already read it + AlreadyRead, + /// User already owns it (not in Codex library though) + AlreadyOwned, +} + +/// Response from `recommendations/dismiss` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecommendationDismissResponse { + /// Whether the dismissal was recorded + pub dismissed: bool, +} + +// ============================================================================= +// Method Validation +// ============================================================================= + +/// Check if a method name is a recommendation method +#[allow(dead_code)] // Protocol contract: mirrors is_storage_method() for recommendation methods +pub fn is_recommendation_method(method: &str) -> bool { + matches!( + method, + "recommendations/get" + | "recommendations/updateProfile" + | "recommendations/clear" + | "recommendations/dismiss" + ) +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + // ========================================================================= + // Recommendation Request Tests + // ========================================================================= + + #[test] + fn test_recommendation_request_serialization() { + let req = RecommendationRequest { + library: vec![UserLibraryEntry { + series_id: "uuid-1".to_string(), + title: "Berserk".to_string(), + alternate_titles: vec![], + year: Some(1989), + status: None, + genres: vec!["Action".to_string(), "Dark Fantasy".to_string()], + tags: vec![], + total_book_count: Some(41), + external_ids: vec![], + reading_status: None, + books_read: 41, + books_owned: 41, + user_rating: Some(95), + user_notes: None, + started_at: None, + last_read_at: None, + completed_at: None, + }], + limit: Some(10), + exclude_ids: vec!["99999".to_string()], + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["library"].as_array().unwrap().len(), 1); + assert_eq!(json["library"][0]["title"], "Berserk"); + assert_eq!(json["limit"], 10); + assert_eq!(json["excludeIds"].as_array().unwrap().len(), 1); + } + + #[test] + fn test_recommendation_request_minimal() { + let json = json!({ + "library": [] + }); + let req: RecommendationRequest = serde_json::from_value(json).unwrap(); + assert!(req.library.is_empty()); + assert!(req.limit.is_none()); + assert!(req.exclude_ids.is_empty()); + } + + #[test] + fn test_recommendation_request_skips_empty_exclude_ids() { + let req = RecommendationRequest { + library: vec![], + limit: None, + exclude_ids: vec![], + }; + let json = serde_json::to_value(&req).unwrap(); + assert!(!json.as_object().unwrap().contains_key("excludeIds")); + assert!(!json.as_object().unwrap().contains_key("limit")); + } + + // ========================================================================= + // Recommendation Response Tests + // ========================================================================= + + #[test] + fn test_recommendation_response_serialization() { + let resp = RecommendationResponse { + recommendations: vec![Recommendation { + external_id: "12345".to_string(), + external_url: Some("https://anilist.co/manga/12345".to_string()), + title: "Vinland Saga".to_string(), + cover_url: Some("https://img.anilist.co/cover.jpg".to_string()), + summary: Some("A Viking epic".to_string()), + genres: vec!["Action".to_string(), "Historical".to_string()], + score: 0.95, + reason: "Because you rated Berserk 10/10".to_string(), + based_on: vec!["Berserk".to_string()], + codex_series_id: None, + in_library: false, + }], + generated_at: Some("2026-02-06T12:00:00Z".to_string()), + cached: false, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["recommendations"].as_array().unwrap().len(), 1); + assert_eq!(json["recommendations"][0]["title"], "Vinland Saga"); + assert_eq!(json["recommendations"][0]["score"], 0.95); + assert_eq!(json["generatedAt"], "2026-02-06T12:00:00Z"); + assert!(!json["cached"].as_bool().unwrap()); + } + + #[test] + fn test_recommendation_response_cached() { + let resp = RecommendationResponse { + recommendations: vec![], + generated_at: Some("2026-02-05T00:00:00Z".to_string()), + cached: true, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["cached"].as_bool().unwrap()); + assert!(json["recommendations"].as_array().unwrap().is_empty()); + } + + #[test] + fn test_recommendation_response_minimal() { + let json = json!({ + "recommendations": [] + }); + let resp: RecommendationResponse = serde_json::from_value(json).unwrap(); + assert!(resp.recommendations.is_empty()); + assert!(resp.generated_at.is_none()); + assert!(!resp.cached); + } + + // ========================================================================= + // Recommendation Tests + // ========================================================================= + + #[test] + fn test_recommendation_full_serialization() { + let rec = Recommendation { + external_id: "54321".to_string(), + external_url: Some("https://anilist.co/manga/54321".to_string()), + title: "Monster".to_string(), + cover_url: Some("https://img.anilist.co/monster.jpg".to_string()), + summary: Some("A psychological thriller".to_string()), + genres: vec!["Thriller".to_string(), "Mystery".to_string()], + score: 0.88, + reason: "Based on your interest in psychological thrillers".to_string(), + based_on: vec!["Death Note".to_string(), "20th Century Boys".to_string()], + codex_series_id: Some("codex-uuid-123".to_string()), + in_library: true, + }; + let json = serde_json::to_value(&rec).unwrap(); + assert_eq!(json["externalId"], "54321"); + assert_eq!(json["externalUrl"], "https://anilist.co/manga/54321"); + assert_eq!(json["title"], "Monster"); + assert_eq!(json["coverUrl"], "https://img.anilist.co/monster.jpg"); + assert_eq!(json["summary"], "A psychological thriller"); + assert_eq!(json["genres"].as_array().unwrap().len(), 2); + assert_eq!(json["score"], 0.88); + assert_eq!( + json["reason"], + "Based on your interest in psychological thrillers" + ); + assert_eq!(json["basedOn"].as_array().unwrap().len(), 2); + assert_eq!(json["codexSeriesId"], "codex-uuid-123"); + assert!(json["inLibrary"].as_bool().unwrap()); + } + + #[test] + fn test_recommendation_minimal() { + let json = json!({ + "externalId": "99", + "title": "Some Manga", + "score": 0.5, + "reason": "You might like it" + }); + let rec: Recommendation = serde_json::from_value(json).unwrap(); + assert_eq!(rec.external_id, "99"); + assert_eq!(rec.title, "Some Manga"); + assert_eq!(rec.score, 0.5); + assert_eq!(rec.reason, "You might like it"); + assert!(rec.external_url.is_none()); + assert!(rec.cover_url.is_none()); + assert!(rec.summary.is_none()); + assert!(rec.genres.is_empty()); + assert!(rec.based_on.is_empty()); + assert!(rec.codex_series_id.is_none()); + assert!(!rec.in_library); + } + + #[test] + fn test_recommendation_skips_none_fields() { + let rec = Recommendation { + external_id: "1".to_string(), + external_url: None, + title: "Test".to_string(), + cover_url: None, + summary: None, + genres: vec![], + score: 0.7, + reason: "test".to_string(), + based_on: vec![], + codex_series_id: None, + in_library: false, + }; + let json = serde_json::to_value(&rec).unwrap(); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("externalUrl")); + assert!(!obj.contains_key("coverUrl")); + assert!(!obj.contains_key("summary")); + assert!(!obj.contains_key("genres")); + assert!(!obj.contains_key("basedOn")); + assert!(!obj.contains_key("codexSeriesId")); + } + + // ========================================================================= + // Profile Update Tests + // ========================================================================= + + #[test] + fn test_profile_update_request_serialization() { + let req = ProfileUpdateRequest { + entries: vec![UserLibraryEntry { + series_id: "uuid-2".to_string(), + title: "One Piece".to_string(), + alternate_titles: vec![], + year: Some(1997), + status: None, + genres: vec!["Adventure".to_string()], + tags: vec![], + total_book_count: None, + external_ids: vec![], + reading_status: None, + books_read: 100, + books_owned: 105, + user_rating: Some(90), + user_notes: None, + started_at: None, + last_read_at: None, + completed_at: None, + }], + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["entries"].as_array().unwrap().len(), 1); + assert_eq!(json["entries"][0]["title"], "One Piece"); + } + + #[test] + fn test_profile_update_response_serialization() { + let resp = ProfileUpdateResponse { + updated: true, + entries_processed: 5, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["updated"].as_bool().unwrap()); + assert_eq!(json["entriesProcessed"], 5); + } + + #[test] + fn test_profile_update_response_deserialization() { + let json = json!({ + "updated": true + }); + let resp: ProfileUpdateResponse = serde_json::from_value(json).unwrap(); + assert!(resp.updated); + assert_eq!(resp.entries_processed, 0); + } + + // ========================================================================= + // Clear Recommendations Tests + // ========================================================================= + + #[test] + fn test_recommendation_clear_response_serialization() { + let resp = RecommendationClearResponse { cleared: true }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["cleared"].as_bool().unwrap()); + } + + #[test] + fn test_recommendation_clear_response_deserialization() { + let json = json!({"cleared": false}); + let resp: RecommendationClearResponse = serde_json::from_value(json).unwrap(); + assert!(!resp.cleared); + } + + // ========================================================================= + // Dismiss Recommendation Tests + // ========================================================================= + + #[test] + fn test_dismiss_request_serialization() { + let req = RecommendationDismissRequest { + external_id: "12345".to_string(), + reason: Some(DismissReason::NotInterested), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["externalId"], "12345"); + assert_eq!(json["reason"], "not_interested"); + } + + #[test] + fn test_dismiss_request_minimal() { + let json = json!({ + "externalId": "99" + }); + let req: RecommendationDismissRequest = serde_json::from_value(json).unwrap(); + assert_eq!(req.external_id, "99"); + assert!(req.reason.is_none()); + } + + #[test] + fn test_dismiss_reason_serialization() { + assert_eq!( + serde_json::to_value(DismissReason::NotInterested).unwrap(), + json!("not_interested") + ); + assert_eq!( + serde_json::to_value(DismissReason::AlreadyRead).unwrap(), + json!("already_read") + ); + assert_eq!( + serde_json::to_value(DismissReason::AlreadyOwned).unwrap(), + json!("already_owned") + ); + } + + #[test] + fn test_dismiss_reason_deserialization() { + let reason: DismissReason = serde_json::from_value(json!("not_interested")).unwrap(); + assert_eq!(reason, DismissReason::NotInterested); + + let reason: DismissReason = serde_json::from_value(json!("already_read")).unwrap(); + assert_eq!(reason, DismissReason::AlreadyRead); + + let reason: DismissReason = serde_json::from_value(json!("already_owned")).unwrap(); + assert_eq!(reason, DismissReason::AlreadyOwned); + } + + #[test] + fn test_dismiss_response_serialization() { + let resp = RecommendationDismissResponse { dismissed: true }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["dismissed"].as_bool().unwrap()); + } + + #[test] + fn test_dismiss_request_skips_none_reason() { + let req = RecommendationDismissRequest { + external_id: "1".to_string(), + reason: None, + }; + let json = serde_json::to_value(&req).unwrap(); + assert!(!json.as_object().unwrap().contains_key("reason")); + } + + // ========================================================================= + // is_recommendation_method Tests + // ========================================================================= + + #[test] + fn test_is_recommendation_method() { + assert!(is_recommendation_method("recommendations/get")); + assert!(is_recommendation_method("recommendations/updateProfile")); + assert!(is_recommendation_method("recommendations/clear")); + assert!(is_recommendation_method("recommendations/dismiss")); + assert!(!is_recommendation_method("sync/getUserInfo")); + assert!(!is_recommendation_method("storage/get")); + assert!(!is_recommendation_method("metadata/series/search")); + assert!(!is_recommendation_method("initialize")); + assert!(!is_recommendation_method("recommendations/unknown")); + } +} diff --git a/src/services/plugin/rpc.rs b/src/services/plugin/rpc.rs index fa2e9983..c0029426 100644 --- a/src/services/plugin/rpc.rs +++ b/src/services/plugin/rpc.rs @@ -1,14 +1,6 @@ //! JSON-RPC Client for Plugin Communication //! //! This module provides a JSON-RPC client that communicates with plugins over stdio. -//! -//! Note: This module provides complete JSON-RPC client infrastructure. -//! Some methods may not be called from external code yet but are part of -//! the complete API for plugin RPC communication. - -// Allow dead code for RPC client infrastructure that is part of the -// complete API surface but not yet fully integrated. -#![allow(dead_code)] use std::collections::HashMap; use std::sync::Arc; @@ -20,12 +12,14 @@ use serde::de::DeserializeOwned; use serde_json::Value; use tokio::sync::{Mutex, oneshot}; use tokio::time::timeout; -use tracing::{debug, error, trace, warn}; +use tracing::{debug, error, warn}; use super::process::{PluginProcess, ProcessError}; use super::protocol::{ JSONRPC_VERSION, JsonRpcError, JsonRpcRequest, JsonRpcResponse, RequestId, error_codes, }; +use super::storage::is_storage_method; +use super::storage_handler::StorageRequestHandler; /// Error type for RPC operations #[derive(Debug, thiserror::Error)] @@ -121,6 +115,28 @@ pub struct RpcClient { impl RpcClient { /// Create a new RPC client wrapping a plugin process pub fn new(process: PluginProcess, default_timeout: Duration) -> Self { + Self::new_internal(process, default_timeout, None) + } + + /// Create a new RPC client with storage request handling support. + /// + /// When a plugin sends a `storage/*` JSON-RPC request on its stdout, + /// the reader task will intercept it, process it via the `StorageRequestHandler`, + /// and write the response back to the plugin's stdin. This enables bidirectional + /// RPC for user plugin storage operations. + pub fn new_with_storage( + process: PluginProcess, + default_timeout: Duration, + storage_handler: StorageRequestHandler, + ) -> Self { + Self::new_internal(process, default_timeout, Some(storage_handler)) + } + + fn new_internal( + process: PluginProcess, + default_timeout: Duration, + storage_handler: Option, + ) -> Self { let process = Arc::new(Mutex::new(process)); let pending: Arc>> = Arc::new(Mutex::new(HashMap::new())); @@ -132,7 +148,7 @@ impl RpcClient { let pending = Arc::clone(&pending); let process_alive = Arc::clone(&process_alive); tokio::spawn(async move { - response_reader_task(process, pending, process_alive).await; + response_reader_task(process, pending, process_alive, storage_handler).await; }) }; @@ -271,44 +287,6 @@ impl RpcClient { self.call::<(), R>(method, ()).await } - /// Send a notification (no response expected) - pub async fn notify

(&self, method: &str, params: P) -> Result<(), RpcError> - where - P: Serialize, - { - let params_value = serde_json::to_value(params)?; - - // Notifications don't have an id - let request = serde_json::json!({ - "jsonrpc": JSONRPC_VERSION, - "method": method, - "params": params_value, - }); - - let request_json = serde_json::to_string(&request)?; - trace!(method, "Sending RPC notification"); - - let process = self.process.lock().await; - process.write_line(&request_json).await?; - Ok(()) - } - - /// Check if the underlying process is still running - pub async fn is_running(&self) -> bool { - // First check the fast atomic flag - if marked dead, don't bother checking process - if !self.process_alive.load(Ordering::Acquire) { - return false; - } - let mut process = self.process.lock().await; - process.is_running() - } - - /// Get the process ID - pub async fn pid(&self) -> Option { - let process = self.process.lock().await; - process.pid() - } - /// Shutdown the RPC client and kill the process pub async fn shutdown(&mut self, timeout_duration: Duration) -> Result { // Mark process as not alive immediately to prevent new requests @@ -339,11 +317,17 @@ impl RpcClient { } } -/// Task that reads responses from the process and dispatches them +/// Task that reads lines from the plugin process and dispatches them. +/// +/// Handles two types of messages: +/// 1. **Responses**: Lines with `result` or `error` → dispatched to pending requests +/// 2. **Reverse RPC requests**: Lines with `method` (e.g., `storage/*`) → handled by +/// the storage handler and response written back to the plugin's stdin async fn response_reader_task( process: Arc>, pending: Arc>>, process_alive: Arc, + storage_handler: Option, ) { debug!("RPC response reader task started"); loop { @@ -374,7 +358,7 @@ async fn response_reader_task( continue; } - // Log the response (truncate for readability, respecting UTF-8 char boundaries) + // Log the line (truncate for readability, respecting UTF-8 char boundaries) let log_preview = if line.len() > 200 { // Find a valid UTF-8 char boundary at or before position 200 let mut end = 200; @@ -387,8 +371,78 @@ async fn response_reader_task( }; debug!(bytes = line.len(), preview = %log_preview, "Received line from plugin"); - // Parse the response - let response: JsonRpcResponse = match serde_json::from_str(&line) { + // Parse as generic JSON to determine if it's a request or response + let json_value: Value = match serde_json::from_str(&line) { + Ok(v) => v, + Err(e) => { + warn!(error = %e, line = %line, "Failed to parse plugin output as JSON"); + continue; + } + }; + + // Check if this is a reverse RPC request from the plugin (has "method" field) + let is_request = json_value + .get("method") + .and_then(|m| m.as_str()) + .map(|m| m.to_string()); + + if let Some(method) = is_request { + if is_storage_method(&method) { + if let Some(ref handler) = storage_handler { + // Parse as a full request + let request: JsonRpcRequest = match serde_json::from_value(json_value) { + Ok(r) => r, + Err(e) => { + warn!(error = %e, "Failed to parse storage request"); + continue; + } + }; + + debug!(method = %method, "Handling reverse RPC storage request from plugin"); + let response = handler.handle_request(&request).await; + + // Write the response back to the plugin's stdin + let response_json = match serde_json::to_string(&response) { + Ok(j) => j, + Err(e) => { + error!(error = %e, "Failed to serialize storage response"); + continue; + } + }; + + let process = process.lock().await; + if let Err(e) = process.write_line(&response_json).await { + error!(error = %e, "Failed to write storage response to plugin"); + } + } else { + warn!( + method = %method, + "Plugin sent storage request but no storage handler is configured" + ); + // Send error response back to plugin + if let Ok(request) = serde_json::from_value::(json_value) { + let error_response = JsonRpcResponse::error( + Some(request.id), + JsonRpcError::new( + error_codes::METHOD_NOT_FOUND, + "Storage is not available for this plugin", + ), + ); + if let Ok(resp_json) = serde_json::to_string(&error_response) { + let process = process.lock().await; + let _ = process.write_line(&resp_json).await; + } + } + } + continue; + } + // Non-storage methods from the plugin are not supported + warn!(method = %method, "Plugin sent unsupported reverse RPC request"); + continue; + } + + // Normal response processing + let response: JsonRpcResponse = match serde_json::from_value(json_value) { Ok(r) => r, Err(e) => { warn!(error = %e, line = %line, "Failed to parse plugin response as JSON-RPC"); diff --git a/src/services/plugin/storage.rs b/src/services/plugin/storage.rs new file mode 100644 index 00000000..4dbc923f --- /dev/null +++ b/src/services/plugin/storage.rs @@ -0,0 +1,324 @@ +//! Plugin Storage Protocol Types +//! +//! Defines the JSON-RPC request/response types for plugin storage operations. +//! Plugins use these methods to persist per-user data like taste profiles, +//! sync state, and cached recommendations. +//! +//! ## Architecture +//! +//! Storage is scoped per user-plugin instance. Plugins only specify a key; +//! the host resolves the user_plugin_id from the connection context. +//! This provides architectural isolation - plugins cannot address other +//! plugins' or users' data. +//! +//! ## Methods +//! +//! - `storage/get` - Get a value by key +//! - `storage/set` - Set a value (upsert) with optional TTL +//! - `storage/delete` - Delete a value by key +//! - `storage/list` - List all keys +//! - `storage/clear` - Clear all data + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +// ============================================================================= +// Storage Request Types +// ============================================================================= + +/// Parameters for `storage/get` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageGetRequest { + /// Storage key to retrieve + pub key: String, +} + +/// Parameters for `storage/set` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageSetRequest { + /// Storage key + pub key: String, + /// JSON data to store + pub data: Value, + /// Optional expiration timestamp (ISO 8601) + /// If set, the data will be automatically cleaned up after this time + #[serde(default, skip_serializing_if = "Option::is_none")] + pub expires_at: Option, +} + +/// Parameters for `storage/delete` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageDeleteRequest { + /// Storage key to delete + pub key: String, +} + +// Note: `storage/list` and `storage/clear` take no parameters + +// ============================================================================= +// Storage Response Types +// ============================================================================= + +/// Response from `storage/get` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageGetResponse { + /// The stored data, or null if key doesn't exist + pub data: Option, + /// Expiration timestamp (ISO 8601) if TTL was set + #[serde(default, skip_serializing_if = "Option::is_none")] + pub expires_at: Option, +} + +/// Response from `storage/set` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageSetResponse { + /// Always true on success + pub success: bool, +} + +/// Response from `storage/delete` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageDeleteResponse { + /// Whether the key existed and was deleted + pub deleted: bool, +} + +/// Individual key entry in `storage/list` response +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageKeyEntry { + /// Storage key name + pub key: String, + /// Expiration timestamp (ISO 8601) if TTL was set + #[serde(default, skip_serializing_if = "Option::is_none")] + pub expires_at: Option, + /// Last update timestamp (ISO 8601) + pub updated_at: String, +} + +/// Response from `storage/list` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageListResponse { + /// All keys for this plugin instance (excluding expired) + pub keys: Vec, +} + +/// Response from `storage/clear` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StorageClearResponse { + /// Number of entries deleted + pub deleted_count: u64, +} + +// ============================================================================= +// Permission Check +// ============================================================================= + +/// Check if a method name is a storage method +pub fn is_storage_method(method: &str) -> bool { + matches!( + method, + "storage/get" | "storage/set" | "storage/delete" | "storage/list" | "storage/clear" + ) +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_storage_get_request_serialization() { + let req = StorageGetRequest { + key: "taste_profile".to_string(), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["key"], "taste_profile"); + } + + #[test] + fn test_storage_get_request_deserialization() { + let json = json!({"key": "sync_state"}); + let req: StorageGetRequest = serde_json::from_value(json).unwrap(); + assert_eq!(req.key, "sync_state"); + } + + #[test] + fn test_storage_set_request_serialization() { + let req = StorageSetRequest { + key: "recommendations".to_string(), + data: json!({"items": [1, 2, 3], "score": 0.95}), + expires_at: Some("2026-02-07T00:00:00Z".to_string()), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["key"], "recommendations"); + assert_eq!(json["data"]["score"], 0.95); + assert_eq!(json["expiresAt"], "2026-02-07T00:00:00Z"); + } + + #[test] + fn test_storage_set_request_without_ttl() { + let req = StorageSetRequest { + key: "profile".to_string(), + data: json!({"version": 1}), + expires_at: None, + }; + let json = serde_json::to_value(&req).unwrap(); + assert!(!json.as_object().unwrap().contains_key("expiresAt")); + } + + #[test] + fn test_storage_set_request_deserialization_with_ttl() { + let json = json!({ + "key": "cache", + "data": [1, 2, 3], + "expiresAt": "2026-03-01T12:00:00Z" + }); + let req: StorageSetRequest = serde_json::from_value(json).unwrap(); + assert_eq!(req.key, "cache"); + assert_eq!(req.data, json!([1, 2, 3])); + assert_eq!(req.expires_at.unwrap(), "2026-03-01T12:00:00Z"); + } + + #[test] + fn test_storage_set_request_deserialization_without_ttl() { + let json = json!({ + "key": "state", + "data": {"cursor": "abc123"} + }); + let req: StorageSetRequest = serde_json::from_value(json).unwrap(); + assert!(req.expires_at.is_none()); + } + + #[test] + fn test_storage_delete_request_serialization() { + let req = StorageDeleteRequest { + key: "old_cache".to_string(), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["key"], "old_cache"); + } + + #[test] + fn test_storage_get_response_with_data() { + let resp = StorageGetResponse { + data: Some(json!({"version": 2, "items": []})), + expires_at: Some("2026-02-08T00:00:00Z".to_string()), + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["data"]["version"], 2); + assert_eq!(json["expiresAt"], "2026-02-08T00:00:00Z"); + } + + #[test] + fn test_storage_get_response_null_data() { + let resp = StorageGetResponse { + data: None, + expires_at: None, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["data"].is_null()); + assert!(!json.as_object().unwrap().contains_key("expiresAt")); + } + + #[test] + fn test_storage_set_response() { + let resp = StorageSetResponse { success: true }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["success"].as_bool().unwrap()); + } + + #[test] + fn test_storage_delete_response() { + let resp = StorageDeleteResponse { deleted: true }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["deleted"].as_bool().unwrap()); + + let resp2 = StorageDeleteResponse { deleted: false }; + let json2 = serde_json::to_value(&resp2).unwrap(); + assert!(!json2["deleted"].as_bool().unwrap()); + } + + #[test] + fn test_storage_list_response() { + let resp = StorageListResponse { + keys: vec![ + StorageKeyEntry { + key: "profile".to_string(), + expires_at: None, + updated_at: "2026-02-06T10:00:00Z".to_string(), + }, + StorageKeyEntry { + key: "cache".to_string(), + expires_at: Some("2026-02-07T00:00:00Z".to_string()), + updated_at: "2026-02-06T11:00:00Z".to_string(), + }, + ], + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["keys"].as_array().unwrap().len(), 2); + assert_eq!(json["keys"][0]["key"], "profile"); + assert!( + !json["keys"][0] + .as_object() + .unwrap() + .contains_key("expiresAt") + ); + assert_eq!(json["keys"][1]["expiresAt"], "2026-02-07T00:00:00Z"); + } + + #[test] + fn test_storage_clear_response() { + let resp = StorageClearResponse { deleted_count: 5 }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["deletedCount"], 5); + } + + #[test] + fn test_is_storage_method() { + assert!(is_storage_method("storage/get")); + assert!(is_storage_method("storage/set")); + assert!(is_storage_method("storage/delete")); + assert!(is_storage_method("storage/list")); + assert!(is_storage_method("storage/clear")); + assert!(!is_storage_method("metadata/series/search")); + assert!(!is_storage_method("initialize")); + assert!(!is_storage_method("storage/unknown")); + } + + #[test] + fn test_storage_get_response_deserialization() { + let json = json!({ + "data": {"genres": ["action", "drama"]}, + "expiresAt": "2026-12-31T23:59:59Z" + }); + let resp: StorageGetResponse = serde_json::from_value(json).unwrap(); + assert!(resp.data.is_some()); + assert_eq!(resp.expires_at.unwrap(), "2026-12-31T23:59:59Z"); + } + + #[test] + fn test_storage_key_entry_serialization() { + let entry = StorageKeyEntry { + key: "test_key".to_string(), + expires_at: None, + updated_at: "2026-02-06T12:00:00Z".to_string(), + }; + let json = serde_json::to_value(&entry).unwrap(); + assert_eq!(json["key"], "test_key"); + assert_eq!(json["updatedAt"], "2026-02-06T12:00:00Z"); + assert!(!json.as_object().unwrap().contains_key("expiresAt")); + } +} diff --git a/src/services/plugin/storage_handler.rs b/src/services/plugin/storage_handler.rs new file mode 100644 index 00000000..ac0738b1 --- /dev/null +++ b/src/services/plugin/storage_handler.rs @@ -0,0 +1,764 @@ +//! Storage Request Handler +//! +//! Processes storage method requests from plugins on the host side. +//! When a plugin sends a `storage/*` JSON-RPC request, the host intercepts it +//! and handles it using the database repository, then sends back the response. +//! +//! This implements the "reverse RPC" pattern where the plugin acts as client +//! and the host acts as server for storage operations. + +use chrono::{DateTime, Utc}; +use sea_orm::DatabaseConnection; +use serde_json::Value; +use tracing::{debug, error, warn}; +use uuid::Uuid; + +use super::protocol::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, error_codes, methods}; +use super::storage::{ + StorageClearResponse, StorageDeleteRequest, StorageDeleteResponse, StorageGetRequest, + StorageGetResponse, StorageKeyEntry, StorageListResponse, StorageSetRequest, + StorageSetResponse, +}; +use crate::db::repositories::UserPluginDataRepository; + +/// Maximum number of storage keys allowed per plugin instance +const MAX_KEYS_PER_PLUGIN: usize = 100; + +/// Maximum serialized size of a single storage value (1 MB) +const MAX_VALUE_SIZE_BYTES: usize = 1_048_576; + +/// Handles storage requests from plugins. +/// +/// This handler is created per-connection with a specific `user_plugin_id`, +/// providing architectural isolation - each handler can only access its own +/// user-plugin instance's data. +#[derive(Clone)] +pub struct StorageRequestHandler { + db: DatabaseConnection, + user_plugin_id: Uuid, +} + +impl StorageRequestHandler { + /// Create a new storage handler for a specific user-plugin instance + pub fn new(db: DatabaseConnection, user_plugin_id: Uuid) -> Self { + Self { db, user_plugin_id } + } + + /// Handle a storage JSON-RPC request and return a response + pub async fn handle_request(&self, request: &JsonRpcRequest) -> JsonRpcResponse { + let id = request.id.clone(); + let method = request.method.as_str(); + + debug!( + method = method, + user_plugin_id = %self.user_plugin_id, + "Handling storage request" + ); + + match method { + methods::STORAGE_GET => self.handle_get(request).await, + methods::STORAGE_SET => self.handle_set(request).await, + methods::STORAGE_DELETE => self.handle_delete(request).await, + methods::STORAGE_LIST => self.handle_list(request).await, + methods::STORAGE_CLEAR => self.handle_clear(request).await, + _ => JsonRpcResponse::error( + Some(id), + JsonRpcError::new( + error_codes::METHOD_NOT_FOUND, + format!("Unknown storage method: {}", method), + ), + ), + } + } + + async fn handle_get(&self, request: &JsonRpcRequest) -> JsonRpcResponse { + let id = request.id.clone(); + + let params: StorageGetRequest = match Self::parse_params(&request.params) { + Ok(p) => p, + Err(resp) => return resp.with_id(id), + }; + + match UserPluginDataRepository::get(&self.db, self.user_plugin_id, ¶ms.key).await { + Ok(Some(entry)) => { + let response = StorageGetResponse { + data: Some(entry.data), + expires_at: entry.expires_at.map(|dt| dt.to_rfc3339()), + }; + JsonRpcResponse::success(id, serde_json::to_value(response).unwrap()) + } + Ok(None) => { + let response = StorageGetResponse { + data: None, + expires_at: None, + }; + JsonRpcResponse::success(id, serde_json::to_value(response).unwrap()) + } + Err(e) => { + error!(error = %e, "Storage get failed"); + JsonRpcResponse::error( + Some(id), + JsonRpcError::new(error_codes::INTERNAL_ERROR, format!("Storage error: {}", e)), + ) + } + } + } + + async fn handle_set(&self, request: &JsonRpcRequest) -> JsonRpcResponse { + let id = request.id.clone(); + + let params: StorageSetRequest = match Self::parse_params(&request.params) { + Ok(p) => p, + Err(resp) => return resp.with_id(id), + }; + + // Enforce value size limit + let serialized_size = serde_json::to_string(¶ms.data) + .map(|s| s.len()) + .unwrap_or(0); + if serialized_size > MAX_VALUE_SIZE_BYTES { + warn!( + user_plugin_id = %self.user_plugin_id, + key = %params.key, + size = serialized_size, + max = MAX_VALUE_SIZE_BYTES, + "Storage value exceeds maximum size" + ); + return JsonRpcResponse::error( + Some(id), + JsonRpcError::new( + error_codes::INVALID_PARAMS, + format!( + "Storage value exceeds maximum size of {}MB", + MAX_VALUE_SIZE_BYTES / 1_048_576 + ), + ), + ); + } + + // Enforce key count limit (only for new keys, not upserts) + let is_new_key = + match UserPluginDataRepository::get(&self.db, self.user_plugin_id, ¶ms.key).await { + Ok(existing) => existing.is_none(), + Err(e) => { + error!(error = %e, "Storage key existence check failed"); + return JsonRpcResponse::error( + Some(id), + JsonRpcError::new( + error_codes::INTERNAL_ERROR, + format!("Storage error: {}", e), + ), + ); + } + }; + + if is_new_key { + match UserPluginDataRepository::list_keys(&self.db, self.user_plugin_id).await { + Ok(keys) => { + if keys.len() >= MAX_KEYS_PER_PLUGIN { + warn!( + user_plugin_id = %self.user_plugin_id, + key_count = keys.len(), + max = MAX_KEYS_PER_PLUGIN, + "Storage key limit exceeded" + ); + return JsonRpcResponse::error( + Some(id), + JsonRpcError::new( + error_codes::INVALID_PARAMS, + format!( + "Storage key limit exceeded (max {} keys)", + MAX_KEYS_PER_PLUGIN + ), + ), + ); + } + } + Err(e) => { + error!(error = %e, "Storage key count check failed"); + return JsonRpcResponse::error( + Some(id), + JsonRpcError::new( + error_codes::INTERNAL_ERROR, + format!("Storage error: {}", e), + ), + ); + } + } + } + + // Parse optional expires_at + let expires_at: Option> = match ¶ms.expires_at { + Some(ts) => match DateTime::parse_from_rfc3339(ts) { + Ok(dt) => Some(dt.with_timezone(&Utc)), + Err(e) => { + warn!(error = %e, timestamp = ts, "Invalid expires_at timestamp"); + return JsonRpcResponse::error( + Some(id), + JsonRpcError::new( + error_codes::INVALID_PARAMS, + format!("Invalid expiresAt timestamp: {}", e), + ), + ); + } + }, + None => None, + }; + + match UserPluginDataRepository::set( + &self.db, + self.user_plugin_id, + ¶ms.key, + params.data, + expires_at, + ) + .await + { + Ok(_) => { + let response = StorageSetResponse { success: true }; + JsonRpcResponse::success(id, serde_json::to_value(response).unwrap()) + } + Err(e) => { + error!(error = %e, "Storage set failed"); + JsonRpcResponse::error( + Some(id), + JsonRpcError::new(error_codes::INTERNAL_ERROR, format!("Storage error: {}", e)), + ) + } + } + } + + async fn handle_delete(&self, request: &JsonRpcRequest) -> JsonRpcResponse { + let id = request.id.clone(); + + let params: StorageDeleteRequest = match Self::parse_params(&request.params) { + Ok(p) => p, + Err(resp) => return resp.with_id(id), + }; + + match UserPluginDataRepository::delete(&self.db, self.user_plugin_id, ¶ms.key).await { + Ok(deleted) => { + let response = StorageDeleteResponse { deleted }; + JsonRpcResponse::success(id, serde_json::to_value(response).unwrap()) + } + Err(e) => { + error!(error = %e, "Storage delete failed"); + JsonRpcResponse::error( + Some(id), + JsonRpcError::new(error_codes::INTERNAL_ERROR, format!("Storage error: {}", e)), + ) + } + } + } + + async fn handle_list(&self, request: &JsonRpcRequest) -> JsonRpcResponse { + let id = request.id.clone(); + + match UserPluginDataRepository::list_keys(&self.db, self.user_plugin_id).await { + Ok(entries) => { + let keys: Vec = entries + .into_iter() + .map(|e| StorageKeyEntry { + key: e.key, + expires_at: e.expires_at.map(|dt| dt.to_rfc3339()), + updated_at: e.updated_at.to_rfc3339(), + }) + .collect(); + let response = StorageListResponse { keys }; + JsonRpcResponse::success(id, serde_json::to_value(response).unwrap()) + } + Err(e) => { + error!(error = %e, "Storage list failed"); + JsonRpcResponse::error( + Some(id), + JsonRpcError::new(error_codes::INTERNAL_ERROR, format!("Storage error: {}", e)), + ) + } + } + } + + async fn handle_clear(&self, request: &JsonRpcRequest) -> JsonRpcResponse { + let id = request.id.clone(); + + match UserPluginDataRepository::clear_all(&self.db, self.user_plugin_id).await { + Ok(count) => { + let response = StorageClearResponse { + deleted_count: count, + }; + JsonRpcResponse::success(id, serde_json::to_value(response).unwrap()) + } + Err(e) => { + error!(error = %e, "Storage clear failed"); + JsonRpcResponse::error( + Some(id), + JsonRpcError::new(error_codes::INTERNAL_ERROR, format!("Storage error: {}", e)), + ) + } + } + } + + /// Parse JSON-RPC params into the expected type + #[allow(clippy::result_large_err)] + fn parse_params( + params: &Option, + ) -> Result { + let params = params.as_ref().ok_or_else(|| { + JsonRpcResponse::error( + None, + JsonRpcError::new(error_codes::INVALID_PARAMS, "params is required"), + ) + })?; + + serde_json::from_value::(params.clone()).map_err(|e| { + JsonRpcResponse::error( + None, + JsonRpcError::new( + error_codes::INVALID_PARAMS, + format!("Invalid params: {}", e), + ), + ) + }) + } +} + +/// Helper trait to set the ID on a response that was created without one +trait WithId { + fn with_id(self, id: super::protocol::RequestId) -> Self; +} + +impl WithId for JsonRpcResponse { + fn with_id(mut self, id: super::protocol::RequestId) -> Self { + self.id = Some(id); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::entities::plugins; + use crate::db::entities::users; + use crate::db::repositories::{PluginsRepository, UserPluginsRepository, UserRepository}; + use crate::db::test_helpers::setup_test_db; + use crate::services::plugin::protocol::RequestId; + use serde_json::json; + + async fn create_test_user(db: &DatabaseConnection) -> users::Model { + let user = users::Model { + id: Uuid::new_v4(), + username: format!("storuser_{}", Uuid::new_v4()), + email: format!("stor_{}@example.com", Uuid::new_v4()), + password_hash: "hash123".to_string(), + role: "reader".to_string(), + is_active: true, + email_verified: false, + permissions: json!([]), + created_at: Utc::now(), + updated_at: Utc::now(), + last_login_at: None, + }; + UserRepository::create(db, &user).await.unwrap() + } + + async fn create_test_plugin(db: &DatabaseConnection) -> plugins::Model { + PluginsRepository::create( + db, + &format!("stor_plugin_{}", Uuid::new_v4()), + "Storage Test Plugin", + Some("A test plugin"), + "user", + "node", + vec!["index.js".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "env", + None, + true, + None, + None, + ) + .await + .unwrap() + } + + async fn setup_handler(db: &DatabaseConnection) -> (StorageRequestHandler, Uuid) { + let user = create_test_user(db).await; + let plugin = create_test_plugin(db).await; + let user_plugin = UserPluginsRepository::create(db, plugin.id, user.id) + .await + .unwrap(); + let handler = StorageRequestHandler::new(db.clone(), user_plugin.id); + (handler, user_plugin.id) + } + + fn make_request(method: &str, params: Option) -> JsonRpcRequest { + JsonRpcRequest::new(1i64, method, params) + } + + #[tokio::test] + async fn test_storage_get_nonexistent() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let request = make_request("storage/get", Some(json!({"key": "missing"}))); + let response = handler.handle_request(&request).await; + + assert!(!response.is_error()); + let result: StorageGetResponse = serde_json::from_value(response.result.unwrap()).unwrap(); + assert!(result.data.is_none()); + } + + #[tokio::test] + async fn test_storage_set_and_get() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Set + let set_req = make_request( + "storage/set", + Some(json!({"key": "profile", "data": {"score": 0.95}})), + ); + let set_resp = handler.handle_request(&set_req).await; + assert!(!set_resp.is_error()); + let set_result: StorageSetResponse = + serde_json::from_value(set_resp.result.unwrap()).unwrap(); + assert!(set_result.success); + + // Get + let get_req = make_request("storage/get", Some(json!({"key": "profile"}))); + let get_resp = handler.handle_request(&get_req).await; + assert!(!get_resp.is_error()); + let get_result: StorageGetResponse = + serde_json::from_value(get_resp.result.unwrap()).unwrap(); + assert_eq!(get_result.data.unwrap(), json!({"score": 0.95})); + } + + #[tokio::test] + async fn test_storage_set_with_ttl() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let set_req = make_request( + "storage/set", + Some(json!({ + "key": "cache", + "data": [1, 2, 3], + "expiresAt": "2030-12-31T23:59:59Z" + })), + ); + let set_resp = handler.handle_request(&set_req).await; + assert!(!set_resp.is_error()); + + let get_req = make_request("storage/get", Some(json!({"key": "cache"}))); + let get_resp = handler.handle_request(&get_req).await; + let result: StorageGetResponse = serde_json::from_value(get_resp.result.unwrap()).unwrap(); + assert_eq!(result.data.unwrap(), json!([1, 2, 3])); + assert!(result.expires_at.is_some()); + } + + #[tokio::test] + async fn test_storage_set_invalid_timestamp() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let set_req = make_request( + "storage/set", + Some(json!({ + "key": "bad", + "data": "test", + "expiresAt": "not-a-timestamp" + })), + ); + let resp = handler.handle_request(&set_req).await; + assert!(resp.is_error()); + assert_eq!(resp.error.unwrap().code, error_codes::INVALID_PARAMS); + } + + #[tokio::test] + async fn test_storage_delete() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Set then delete + let set_req = make_request("storage/set", Some(json!({"key": "temp", "data": "value"}))); + handler.handle_request(&set_req).await; + + let del_req = make_request("storage/delete", Some(json!({"key": "temp"}))); + let del_resp = handler.handle_request(&del_req).await; + assert!(!del_resp.is_error()); + let result: StorageDeleteResponse = + serde_json::from_value(del_resp.result.unwrap()).unwrap(); + assert!(result.deleted); + + // Delete nonexistent + let del_req2 = make_request("storage/delete", Some(json!({"key": "nope"}))); + let del_resp2 = handler.handle_request(&del_req2).await; + let result2: StorageDeleteResponse = + serde_json::from_value(del_resp2.result.unwrap()).unwrap(); + assert!(!result2.deleted); + } + + #[tokio::test] + async fn test_storage_list() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Set some keys + for key in &["alpha", "beta", "gamma"] { + let req = make_request("storage/set", Some(json!({"key": key, "data": key}))); + handler.handle_request(&req).await; + } + + let list_req = make_request("storage/list", None); + let list_resp = handler.handle_request(&list_req).await; + assert!(!list_resp.is_error()); + let result: StorageListResponse = + serde_json::from_value(list_resp.result.unwrap()).unwrap(); + assert_eq!(result.keys.len(), 3); + assert_eq!(result.keys[0].key, "alpha"); + assert_eq!(result.keys[1].key, "beta"); + assert_eq!(result.keys[2].key, "gamma"); + } + + #[tokio::test] + async fn test_storage_clear() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Set some keys + for key in &["a", "b", "c"] { + let req = make_request("storage/set", Some(json!({"key": key, "data": 1}))); + handler.handle_request(&req).await; + } + + let clear_req = make_request("storage/clear", None); + let clear_resp = handler.handle_request(&clear_req).await; + assert!(!clear_resp.is_error()); + let result: StorageClearResponse = + serde_json::from_value(clear_resp.result.unwrap()).unwrap(); + assert_eq!(result.deleted_count, 3); + + // Verify empty + let list_req = make_request("storage/list", None); + let list_resp = handler.handle_request(&list_req).await; + let list_result: StorageListResponse = + serde_json::from_value(list_resp.result.unwrap()).unwrap(); + assert!(list_result.keys.is_empty()); + } + + #[tokio::test] + async fn test_storage_missing_params() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let req = make_request("storage/get", None); + let resp = handler.handle_request(&req).await; + assert!(resp.is_error()); + assert_eq!(resp.error.unwrap().code, error_codes::INVALID_PARAMS); + } + + #[tokio::test] + async fn test_storage_invalid_params() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let req = make_request("storage/get", Some(json!({"wrong_field": "test"}))); + let resp = handler.handle_request(&req).await; + assert!(resp.is_error()); + assert_eq!(resp.error.unwrap().code, error_codes::INVALID_PARAMS); + } + + #[tokio::test] + async fn test_storage_unknown_method() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let req = make_request("storage/unknown", Some(json!({}))); + let resp = handler.handle_request(&req).await; + assert!(resp.is_error()); + assert_eq!(resp.error.unwrap().code, error_codes::METHOD_NOT_FOUND); + } + + #[tokio::test] + async fn test_storage_data_isolation() { + let db = setup_test_db().await; + + // Create two handlers (different user-plugin instances) + let (handler1, _) = setup_handler(&db).await; + let (handler2, _) = setup_handler(&db).await; + + // Set same key in both + let set1 = make_request( + "storage/set", + Some(json!({"key": "shared_key", "data": {"owner": "user1"}})), + ); + handler1.handle_request(&set1).await; + + let set2 = make_request( + "storage/set", + Some(json!({"key": "shared_key", "data": {"owner": "user2"}})), + ); + handler2.handle_request(&set2).await; + + // Each should see their own data + let get1 = make_request("storage/get", Some(json!({"key": "shared_key"}))); + let resp1 = handler1.handle_request(&get1).await; + let data1: StorageGetResponse = serde_json::from_value(resp1.result.unwrap()).unwrap(); + assert_eq!(data1.data.unwrap(), json!({"owner": "user1"})); + + let get2 = make_request("storage/get", Some(json!({"key": "shared_key"}))); + let resp2 = handler2.handle_request(&get2).await; + let data2: StorageGetResponse = serde_json::from_value(resp2.result.unwrap()).unwrap(); + assert_eq!(data2.data.unwrap(), json!({"owner": "user2"})); + } + + #[tokio::test] + async fn test_storage_upsert() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Set initial + let set1 = make_request( + "storage/set", + Some(json!({"key": "version", "data": {"v": 1}})), + ); + handler.handle_request(&set1).await; + + // Upsert + let set2 = make_request( + "storage/set", + Some(json!({"key": "version", "data": {"v": 2}})), + ); + handler.handle_request(&set2).await; + + // Verify updated + let get = make_request("storage/get", Some(json!({"key": "version"}))); + let resp = handler.handle_request(&get).await; + let result: StorageGetResponse = serde_json::from_value(resp.result.unwrap()).unwrap(); + assert_eq!(result.data.unwrap(), json!({"v": 2})); + } + + #[tokio::test] + async fn test_storage_list_empty() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let list_req = make_request("storage/list", None); + let resp = handler.handle_request(&list_req).await; + assert!(!resp.is_error()); + let result: StorageListResponse = serde_json::from_value(resp.result.unwrap()).unwrap(); + assert!(result.keys.is_empty()); + } + + #[tokio::test] + async fn test_response_has_correct_id() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + let request = JsonRpcRequest::new(42i64, "storage/get", Some(json!({"key": "test"}))); + let response = handler.handle_request(&request).await; + assert_eq!(response.id, Some(RequestId::Number(42))); + + let request2 = JsonRpcRequest::new("abc".to_string(), "storage/list", None); + let response2 = handler.handle_request(&request2).await; + assert_eq!(response2.id, Some(RequestId::String("abc".to_string()))); + } + + #[tokio::test] + async fn test_storage_set_value_size_limit() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Create a value that exceeds 1MB when serialized + let large_value = "x".repeat(MAX_VALUE_SIZE_BYTES + 1); + let req = make_request( + "storage/set", + Some(json!({"key": "big", "data": large_value})), + ); + let resp = handler.handle_request(&req).await; + + assert!(resp.is_error()); + let err = resp.error.unwrap(); + assert_eq!(err.code, error_codes::INVALID_PARAMS); + assert!(err.message.contains("maximum size")); + } + + #[tokio::test] + async fn test_storage_set_value_at_limit_succeeds() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // A small value should work fine + let req = make_request( + "storage/set", + Some(json!({"key": "small", "data": "hello world"})), + ); + let resp = handler.handle_request(&req).await; + assert!(!resp.is_error()); + } + + #[tokio::test] + async fn test_storage_set_key_count_limit() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Fill up to the key limit + for i in 0..MAX_KEYS_PER_PLUGIN { + let req = make_request( + "storage/set", + Some(json!({"key": format!("key_{}", i), "data": i})), + ); + let resp = handler.handle_request(&req).await; + assert!( + !resp.is_error(), + "Failed to set key_{}: {:?}", + i, + resp.error + ); + } + + // Attempting to add one more new key should fail + let req = make_request( + "storage/set", + Some(json!({"key": "one_too_many", "data": "overflow"})), + ); + let resp = handler.handle_request(&req).await; + assert!(resp.is_error()); + let err = resp.error.unwrap(); + assert_eq!(err.code, error_codes::INVALID_PARAMS); + assert!(err.message.contains("key limit exceeded")); + } + + #[tokio::test] + async fn test_storage_upsert_at_key_limit_succeeds() { + let db = setup_test_db().await; + let (handler, _) = setup_handler(&db).await; + + // Fill up to the key limit + for i in 0..MAX_KEYS_PER_PLUGIN { + let req = make_request( + "storage/set", + Some(json!({"key": format!("key_{}", i), "data": i})), + ); + handler.handle_request(&req).await; + } + + // Upsert an existing key should succeed even at the limit + let req = make_request( + "storage/set", + Some(json!({"key": "key_0", "data": "updated"})), + ); + let resp = handler.handle_request(&req).await; + assert!(!resp.is_error(), "Upsert at key limit should succeed"); + + // Verify the value was updated + let get_req = make_request("storage/get", Some(json!({"key": "key_0"}))); + let get_resp = handler.handle_request(&get_req).await; + let result: StorageGetResponse = serde_json::from_value(get_resp.result.unwrap()).unwrap(); + assert_eq!(result.data.unwrap(), json!("updated")); + } +} diff --git a/src/services/plugin/sync.rs b/src/services/plugin/sync.rs new file mode 100644 index 00000000..17430407 --- /dev/null +++ b/src/services/plugin/sync.rs @@ -0,0 +1,778 @@ +//! Sync Provider Protocol Types +//! +//! Defines the JSON-RPC request/response types for sync provider operations. +//! Sync providers push and pull reading progress between Codex and external +//! services like AniList and MyAnimeList. +//! +//! ## Architecture +//! +//! Sync operations are initiated by the host (Codex) and sent to the plugin. +//! The plugin communicates with the external service using user credentials +//! provided during initialization. +//! +//! ## Methods +//! +//! - `sync/getUserInfo` - Get user info from external service +//! - `sync/pushProgress` - Push reading progress to external service +//! - `sync/pullProgress` - Pull reading progress from external service +//! - `sync/status` - Get sync status/diff between Codex and external + +use serde::{Deserialize, Serialize}; + +// ============================================================================= +// Reading Status +// ============================================================================= + +/// Reading status for sync entries +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SyncReadingStatus { + /// Currently reading + Reading, + /// Finished reading + Completed, + /// Paused / On hold + OnHold, + /// Dropped / Abandoned + Dropped, + /// Planning to read + PlanToRead, +} + +// ============================================================================= +// User Info +// ============================================================================= + +/// Response from `sync/getUserInfo` method +/// +/// Returns the user's identity on the external service. +/// Used to display the connected account in the UI. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExternalUserInfo { + /// User ID on the external service + pub external_id: String, + /// Display name / username + pub username: String, + /// Avatar/profile image URL + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + /// Profile URL on the external service + #[serde(default, skip_serializing_if = "Option::is_none")] + pub profile_url: Option, +} + +// ============================================================================= +// Sync Entry (shared between push and pull) +// ============================================================================= + +/// A single reading progress entry for sync +/// +/// Represents one series/book's reading state that can be pushed to +/// or pulled from an external service. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncEntry { + /// External ID on the target service (e.g., AniList media ID) + pub external_id: String, + /// Reading status + pub status: SyncReadingStatus, + /// Reading progress + #[serde(default, skip_serializing_if = "Option::is_none")] + pub progress: Option, + /// User's score/rating (service-specific scale, e.g., 1-10 or 1-100) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub score: Option, + /// When the user started reading (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + /// When the user completed reading (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub completed_at: Option, + /// User notes + #[serde(default, skip_serializing_if = "Option::is_none")] + pub notes: Option, + /// When the series was most recently updated (ISO 8601). + /// Populated from the most recent read_progress.updated_at for the series. + /// Plugins can use this for time-based logic (e.g., pause/drop stale series). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub latest_updated_at: Option, + /// Series title (for plugins that support title-based search fallback). + /// Populated when the backend knows the series name. Plugins can use this + /// to search the external service by title when no external ID is present. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, +} + +/// Reading progress details +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncProgress { + /// Number of chapters read + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chapters: Option, + /// Number of volumes read + #[serde(default, skip_serializing_if = "Option::is_none")] + pub volumes: Option, + /// Number of pages read (for single-volume works) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pages: Option, + /// Total number of chapters in the series (if known) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_chapters: Option, + /// Total number of volumes in the series (if known) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_volumes: Option, +} + +// ============================================================================= +// Push Progress +// ============================================================================= + +/// Parameters for `sync/pushProgress` method +/// +/// Sends reading progress from Codex to the external service. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncPushRequest { + /// Entries to push to the external service + pub entries: Vec, +} + +/// Response from `sync/pushProgress` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncPushResponse { + /// Successfully synced entries + pub success: Vec, + /// Failed entries + pub failed: Vec, +} + +/// Result for a single sync entry (push or pull) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncEntryResult { + /// External ID of the entry + pub external_id: String, + /// Result status + pub status: SyncEntryResultStatus, + /// Error message if failed + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Status of a single sync entry operation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SyncEntryResultStatus { + /// New entry created on external service + Created, + /// Existing entry updated + Updated, + /// No changes needed + Unchanged, + /// Operation failed + Failed, +} + +// ============================================================================= +// Pull Progress +// ============================================================================= + +/// Parameters for `sync/pullProgress` method +/// +/// Requests reading progress from the external service. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncPullRequest { + /// Only pull entries updated after this timestamp (ISO 8601) + /// If not set, pulls all entries + #[serde(default, skip_serializing_if = "Option::is_none")] + pub since: Option, + /// Maximum number of entries to pull + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Pagination cursor for continuing a previous pull + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cursor: Option, +} + +/// Response from `sync/pullProgress` method +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncPullResponse { + /// Entries pulled from the external service + pub entries: Vec, + /// Cursor for next page (if more entries available) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + /// Whether there are more entries to pull + #[serde(default)] + pub has_more: bool, +} + +// ============================================================================= +// Sync Status +// ============================================================================= + +/// Response from `sync/status` method +/// +/// Provides an overview of the sync state between Codex and the external service. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncStatusResponse { + /// Last successful sync timestamp (ISO 8601) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_sync_at: Option, + /// Number of entries on the external service + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_count: Option, + /// Number of entries that need to be pushed + #[serde(default)] + pub pending_push: u32, + /// Number of entries that need to be pulled + #[serde(default)] + pub pending_pull: u32, + /// Entries with conflicts (different on both sides) + #[serde(default)] + pub conflicts: u32, +} + +// ============================================================================= +// Permission Check +// ============================================================================= + +/// Check if a method name is a sync method +#[allow(dead_code)] // Protocol contract: mirrors is_storage_method() for sync methods +pub fn is_sync_method(method: &str) -> bool { + matches!( + method, + "sync/getUserInfo" | "sync/pushProgress" | "sync/pullProgress" | "sync/status" + ) +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + // ========================================================================= + // Reading Status Tests + // ========================================================================= + + #[test] + fn test_reading_status_serialization() { + assert_eq!( + serde_json::to_value(SyncReadingStatus::Reading).unwrap(), + json!("reading") + ); + assert_eq!( + serde_json::to_value(SyncReadingStatus::Completed).unwrap(), + json!("completed") + ); + assert_eq!( + serde_json::to_value(SyncReadingStatus::OnHold).unwrap(), + json!("on_hold") + ); + assert_eq!( + serde_json::to_value(SyncReadingStatus::Dropped).unwrap(), + json!("dropped") + ); + assert_eq!( + serde_json::to_value(SyncReadingStatus::PlanToRead).unwrap(), + json!("plan_to_read") + ); + } + + #[test] + fn test_reading_status_deserialization() { + let reading: SyncReadingStatus = serde_json::from_value(json!("reading")).unwrap(); + assert_eq!(reading, SyncReadingStatus::Reading); + + let on_hold: SyncReadingStatus = serde_json::from_value(json!("on_hold")).unwrap(); + assert_eq!(on_hold, SyncReadingStatus::OnHold); + + let plan: SyncReadingStatus = serde_json::from_value(json!("plan_to_read")).unwrap(); + assert_eq!(plan, SyncReadingStatus::PlanToRead); + } + + // ========================================================================= + // External User Info Tests + // ========================================================================= + + #[test] + fn test_external_user_info_serialization() { + let info = ExternalUserInfo { + external_id: "12345".to_string(), + username: "manga_reader".to_string(), + avatar_url: Some("https://anilist.co/img/avatar.jpg".to_string()), + profile_url: Some("https://anilist.co/user/manga_reader".to_string()), + }; + let json = serde_json::to_value(&info).unwrap(); + assert_eq!(json["externalId"], "12345"); + assert_eq!(json["username"], "manga_reader"); + assert_eq!(json["avatarUrl"], "https://anilist.co/img/avatar.jpg"); + assert_eq!(json["profileUrl"], "https://anilist.co/user/manga_reader"); + } + + #[test] + fn test_external_user_info_minimal() { + let json = json!({ + "externalId": "99", + "username": "user99" + }); + let info: ExternalUserInfo = serde_json::from_value(json).unwrap(); + assert_eq!(info.external_id, "99"); + assert_eq!(info.username, "user99"); + assert!(info.avatar_url.is_none()); + assert!(info.profile_url.is_none()); + } + + #[test] + fn test_external_user_info_skips_none_fields() { + let info = ExternalUserInfo { + external_id: "1".to_string(), + username: "test".to_string(), + avatar_url: None, + profile_url: None, + }; + let json = serde_json::to_value(&info).unwrap(); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("avatarUrl")); + assert!(!obj.contains_key("profileUrl")); + } + + // ========================================================================= + // Sync Entry Tests + // ========================================================================= + + #[test] + fn test_sync_entry_full_serialization() { + let entry = SyncEntry { + external_id: "12345".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: Some(42), + volumes: Some(5), + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: Some(8.5), + started_at: Some("2026-01-15T00:00:00Z".to_string()), + completed_at: None, + notes: Some("Great series!".to_string()), + latest_updated_at: Some("2026-02-01T12:00:00Z".to_string()), + title: None, + }; + let json = serde_json::to_value(&entry).unwrap(); + assert_eq!(json["externalId"], "12345"); + assert_eq!(json["status"], "reading"); + assert_eq!(json["progress"]["chapters"], 42); + assert_eq!(json["progress"]["volumes"], 5); + assert!(!json["progress"].as_object().unwrap().contains_key("pages")); + assert_eq!(json["score"], 8.5); + assert_eq!(json["startedAt"], "2026-01-15T00:00:00Z"); + assert!(!json.as_object().unwrap().contains_key("completedAt")); + assert_eq!(json["notes"], "Great series!"); + assert_eq!(json["latestUpdatedAt"], "2026-02-01T12:00:00Z"); + } + + #[test] + fn test_sync_entry_minimal() { + let json = json!({ + "externalId": "99", + "status": "completed" + }); + let entry: SyncEntry = serde_json::from_value(json).unwrap(); + assert_eq!(entry.external_id, "99"); + assert_eq!(entry.status, SyncReadingStatus::Completed); + assert!(entry.progress.is_none()); + assert!(entry.score.is_none()); + assert!(entry.started_at.is_none()); + assert!(entry.completed_at.is_none()); + assert!(entry.notes.is_none()); + } + + #[test] + fn test_sync_progress_serialization() { + let progress = SyncProgress { + chapters: Some(100), + volumes: Some(10), + pages: Some(3200), + total_chapters: None, + total_volumes: None, + }; + let json = serde_json::to_value(&progress).unwrap(); + assert_eq!(json["chapters"], 100); + assert_eq!(json["volumes"], 10); + assert_eq!(json["pages"], 3200); + } + + #[test] + fn test_sync_progress_partial() { + let progress = SyncProgress { + chapters: Some(50), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }; + let json = serde_json::to_value(&progress).unwrap(); + assert_eq!(json["chapters"], 50); + assert!(!json.as_object().unwrap().contains_key("volumes")); + assert!(!json.as_object().unwrap().contains_key("pages")); + } + + #[test] + fn test_sync_progress_with_totals() { + let progress = SyncProgress { + chapters: Some(42), + volumes: Some(5), + pages: None, + total_chapters: Some(200), + total_volumes: Some(20), + }; + let json = serde_json::to_value(&progress).unwrap(); + assert_eq!(json["chapters"], 42); + assert_eq!(json["volumes"], 5); + assert_eq!(json["totalChapters"], 200); + assert_eq!(json["totalVolumes"], 20); + assert!(!json.as_object().unwrap().contains_key("pages")); + } + + #[test] + fn test_sync_progress_totals_deserialization() { + let json = json!({ + "chapters": 10, + "totalChapters": 100, + "totalVolumes": 10 + }); + let progress: SyncProgress = serde_json::from_value(json).unwrap(); + assert_eq!(progress.chapters, Some(10)); + assert_eq!(progress.total_chapters, Some(100)); + assert_eq!(progress.total_volumes, Some(10)); + assert!(progress.volumes.is_none()); + assert!(progress.pages.is_none()); + } + + // ========================================================================= + // Push Progress Tests + // ========================================================================= + + #[test] + fn test_sync_push_request_serialization() { + let req = SyncPushRequest { + entries: vec![ + SyncEntry { + external_id: "1".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: Some(10), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }, + SyncEntry { + external_id: "2".to_string(), + status: SyncReadingStatus::Completed, + progress: None, + score: Some(9.0), + started_at: None, + completed_at: Some("2026-02-01T00:00:00Z".to_string()), + notes: None, + latest_updated_at: None, + title: None, + }, + ], + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["entries"].as_array().unwrap().len(), 2); + assert_eq!(json["entries"][0]["externalId"], "1"); + assert_eq!(json["entries"][1]["status"], "completed"); + } + + #[test] + fn test_sync_push_response_serialization() { + let resp = SyncPushResponse { + success: vec![ + SyncEntryResult { + external_id: "1".to_string(), + status: SyncEntryResultStatus::Updated, + error: None, + }, + SyncEntryResult { + external_id: "2".to_string(), + status: SyncEntryResultStatus::Created, + error: None, + }, + ], + failed: vec![SyncEntryResult { + external_id: "3".to_string(), + status: SyncEntryResultStatus::Failed, + error: Some("Rate limited".to_string()), + }], + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["success"].as_array().unwrap().len(), 2); + assert_eq!(json["success"][0]["status"], "updated"); + assert_eq!(json["success"][1]["status"], "created"); + assert_eq!(json["failed"].as_array().unwrap().len(), 1); + assert_eq!(json["failed"][0]["status"], "failed"); + assert_eq!(json["failed"][0]["error"], "Rate limited"); + } + + #[test] + fn test_sync_entry_result_status_serialization() { + assert_eq!( + serde_json::to_value(SyncEntryResultStatus::Created).unwrap(), + json!("created") + ); + assert_eq!( + serde_json::to_value(SyncEntryResultStatus::Updated).unwrap(), + json!("updated") + ); + assert_eq!( + serde_json::to_value(SyncEntryResultStatus::Unchanged).unwrap(), + json!("unchanged") + ); + assert_eq!( + serde_json::to_value(SyncEntryResultStatus::Failed).unwrap(), + json!("failed") + ); + } + + #[test] + fn test_sync_entry_result_skips_none_error() { + let result = SyncEntryResult { + external_id: "1".to_string(), + status: SyncEntryResultStatus::Updated, + error: None, + }; + let json = serde_json::to_value(&result).unwrap(); + assert!(!json.as_object().unwrap().contains_key("error")); + } + + // ========================================================================= + // Pull Progress Tests + // ========================================================================= + + #[test] + fn test_sync_pull_request_serialization() { + let req = SyncPullRequest { + since: Some("2026-02-01T00:00:00Z".to_string()), + limit: Some(50), + cursor: None, + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["since"], "2026-02-01T00:00:00Z"); + assert_eq!(json["limit"], 50); + assert!(!json.as_object().unwrap().contains_key("cursor")); + } + + #[test] + fn test_sync_pull_request_minimal() { + let json = json!({}); + let req: SyncPullRequest = serde_json::from_value(json).unwrap(); + assert!(req.since.is_none()); + assert!(req.limit.is_none()); + assert!(req.cursor.is_none()); + } + + #[test] + fn test_sync_pull_request_with_cursor() { + let req = SyncPullRequest { + since: None, + limit: None, + cursor: Some("next_page_token".to_string()), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["cursor"], "next_page_token"); + } + + #[test] + fn test_sync_pull_response_serialization() { + let resp = SyncPullResponse { + entries: vec![SyncEntry { + external_id: "42".to_string(), + status: SyncReadingStatus::OnHold, + progress: Some(SyncProgress { + chapters: Some(25), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: Some(7.0), + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }], + next_cursor: Some("page2".to_string()), + has_more: true, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["entries"].as_array().unwrap().len(), 1); + assert_eq!(json["entries"][0]["status"], "on_hold"); + assert_eq!(json["nextCursor"], "page2"); + assert!(json["hasMore"].as_bool().unwrap()); + } + + #[test] + fn test_sync_pull_response_last_page() { + let resp = SyncPullResponse { + entries: vec![], + next_cursor: None, + has_more: false, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json["entries"].as_array().unwrap().is_empty()); + assert!(!json.as_object().unwrap().contains_key("nextCursor")); + assert!(!json["hasMore"].as_bool().unwrap()); + } + + // ========================================================================= + // Sync Status Tests + // ========================================================================= + + #[test] + fn test_sync_status_response_full() { + let resp = SyncStatusResponse { + last_sync_at: Some("2026-02-06T12:00:00Z".to_string()), + external_count: Some(150), + pending_push: 5, + pending_pull: 3, + conflicts: 1, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["lastSyncAt"], "2026-02-06T12:00:00Z"); + assert_eq!(json["externalCount"], 150); + assert_eq!(json["pendingPush"], 5); + assert_eq!(json["pendingPull"], 3); + assert_eq!(json["conflicts"], 1); + } + + #[test] + fn test_sync_status_response_minimal() { + let json = json!({}); + let resp: SyncStatusResponse = serde_json::from_value(json).unwrap(); + assert!(resp.last_sync_at.is_none()); + assert!(resp.external_count.is_none()); + assert_eq!(resp.pending_push, 0); + assert_eq!(resp.pending_pull, 0); + assert_eq!(resp.conflicts, 0); + } + + #[test] + fn test_sync_status_skips_none_fields() { + let resp = SyncStatusResponse { + last_sync_at: None, + external_count: None, + pending_push: 0, + pending_pull: 0, + conflicts: 0, + }; + let json = serde_json::to_value(&resp).unwrap(); + let obj = json.as_object().unwrap(); + assert!(!obj.contains_key("lastSyncAt")); + assert!(!obj.contains_key("externalCount")); + } + + // ========================================================================= + // SyncEntry title field Tests + // ========================================================================= + + #[test] + fn test_sync_entry_with_title() { + let entry = SyncEntry { + external_id: "".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: None, + volumes: Some(3), + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: Some("Berserk".to_string()), + }; + let json = serde_json::to_value(&entry).unwrap(); + assert_eq!(json["title"], "Berserk"); + assert_eq!(json["externalId"], ""); + } + + #[test] + fn test_sync_entry_title_omitted_when_none() { + let entry = SyncEntry { + external_id: "42".to_string(), + status: SyncReadingStatus::Reading, + progress: None, + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }; + let json = serde_json::to_value(&entry).unwrap(); + assert!(!json.as_object().unwrap().contains_key("title")); + } + + #[test] + fn test_sync_entry_title_deserialization() { + let json = json!({ + "externalId": "", + "status": "reading", + "title": "One Piece" + }); + let entry: SyncEntry = serde_json::from_value(json).unwrap(); + assert_eq!(entry.title, Some("One Piece".to_string())); + assert_eq!(entry.external_id, ""); + } + + #[test] + fn test_sync_entry_title_absent_deserializes_to_none() { + let json = json!({ + "externalId": "42", + "status": "completed" + }); + let entry: SyncEntry = serde_json::from_value(json).unwrap(); + assert!(entry.title.is_none()); + } + + // ========================================================================= + // is_sync_method Tests + // ========================================================================= + + #[test] + fn test_is_sync_method() { + assert!(is_sync_method("sync/getUserInfo")); + assert!(is_sync_method("sync/pushProgress")); + assert!(is_sync_method("sync/pullProgress")); + assert!(is_sync_method("sync/status")); + assert!(!is_sync_method("storage/get")); + assert!(!is_sync_method("metadata/series/search")); + assert!(!is_sync_method("initialize")); + assert!(!is_sync_method("sync/unknown")); + } +} diff --git a/src/services/user_plugin/mod.rs b/src/services/user_plugin/mod.rs new file mode 100644 index 00000000..d0e70aa5 --- /dev/null +++ b/src/services/user_plugin/mod.rs @@ -0,0 +1,14 @@ +//! User Plugin Services +//! +//! This module provides services for managing user-level plugin integrations: +//! - OAuth 2.0 authentication flows (authorization, token exchange, CSRF protection) +//! - Token refresh for expiring OAuth tokens +//! +//! User plugins differ from system plugins in that each user has their own +//! credentials and configuration. The services in this module handle the +//! per-user aspects of plugin management. + +pub mod oauth; +pub mod token_refresh; + +pub use oauth::OAuthStateManager; diff --git a/src/services/user_plugin/oauth.rs b/src/services/user_plugin/oauth.rs new file mode 100644 index 00000000..7cbaf1a7 --- /dev/null +++ b/src/services/user_plugin/oauth.rs @@ -0,0 +1,586 @@ +//! OAuth 2.0 State Management for User Plugins +//! +//! Handles CSRF protection via state parameter, PKCE challenge generation, +//! and authorization URL construction for plugin OAuth flows. + +use anyhow::{Result, anyhow}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use chrono::{DateTime, Duration, Utc}; +use dashmap::DashMap; +use rand::RngCore; +use serde::Deserialize; +use std::sync::Arc; +use tracing::{debug, warn}; +use uuid::Uuid; + +use crate::services::plugin::protocol::OAuthConfig; + +/// Duration for pending OAuth state (5 minutes) +const OAUTH_STATE_TTL_SECS: i64 = 300; + +/// Pending OAuth flow state +#[derive(Debug, Clone)] +pub struct PendingOAuthFlow { + /// Plugin ID this OAuth flow is for + pub plugin_id: Uuid, + /// User ID who initiated the flow + pub user_id: Uuid, + /// PKCE code verifier (needed for token exchange) + pub pkce_verifier: Option, + /// PKCE code challenge (sent in auth URL, kept for debugging/logging) + #[allow(dead_code)] + pub pkce_challenge: Option, + /// The redirect URI used in the authorization request (must match in token exchange) + pub redirect_uri: String, + /// When this state was created + pub created_at: DateTime, +} + +/// OAuth token response from the token endpoint +#[derive(Debug, Clone, Deserialize)] +pub struct OAuthTokenResponse { + pub access_token: String, + #[serde(default)] + pub refresh_token: Option, + #[serde(default)] + pub expires_in: Option, + #[serde(default)] + #[allow(dead_code)] + pub token_type: Option, + #[serde(default)] + pub scope: Option, +} + +/// Result of a completed OAuth flow +#[derive(Debug, Clone)] +pub struct OAuthResult { + pub access_token: String, + pub refresh_token: Option, + pub expires_at: Option>, + pub scope: Option, +} + +/// OAuth state manager for tracking pending OAuth flows +#[derive(Clone)] +pub struct OAuthStateManager { + /// Map of state parameter -> pending flow + pending_flows: Arc>, +} + +impl OAuthStateManager { + pub fn new() -> Self { + Self { + pending_flows: Arc::new(DashMap::new()), + } + } + + /// Generate a cryptographically random state parameter + fn generate_state() -> String { + let mut bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + URL_SAFE_NO_PAD.encode(bytes) + } + + /// Generate a PKCE code verifier and challenge + fn generate_pkce() -> (String, String) { + // Generate 32 bytes of random data for code verifier + let mut verifier_bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut verifier_bytes); + let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); + + // S256 challenge: BASE64URL(SHA256(verifier)) + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(verifier.as_bytes()); + let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize()); + + (verifier, challenge) + } + + /// Build the authorization URL for a plugin's OAuth flow + /// + /// Returns (authorization_url, state_token) + pub fn start_oauth_flow( + &self, + plugin_id: Uuid, + user_id: Uuid, + oauth_config: &OAuthConfig, + client_id: &str, + redirect_uri: &str, + ) -> Result<(String, String)> { + // Generate state for CSRF protection + let state = Self::generate_state(); + + // Generate PKCE if enabled + let (pkce_verifier, pkce_challenge) = if oauth_config.pkce { + let (v, c) = Self::generate_pkce(); + (Some(v), Some(c)) + } else { + (None, None) + }; + + // Build authorization URL + let mut auth_url = format!( + "{}?response_type=code&client_id={}&redirect_uri={}&state={}", + oauth_config.authorization_url, + urlencoding::encode(client_id), + urlencoding::encode(redirect_uri), + urlencoding::encode(&state), + ); + + // Add scopes if present + if !oauth_config.scopes.is_empty() { + auth_url.push_str(&format!( + "&scope={}", + urlencoding::encode(&oauth_config.scopes.join(" ")) + )); + } + + // Add PKCE challenge if enabled + if let Some(ref challenge) = pkce_challenge { + auth_url.push_str(&format!( + "&code_challenge={}&code_challenge_method=S256", + urlencoding::encode(challenge) + )); + } + + // Store pending flow + let pending = PendingOAuthFlow { + plugin_id, + user_id, + pkce_verifier, + pkce_challenge, + redirect_uri: redirect_uri.to_string(), + created_at: Utc::now(), + }; + + self.pending_flows.insert(state.clone(), pending); + + debug!( + plugin_id = %plugin_id, + user_id = %user_id, + "Started OAuth flow with state" + ); + + Ok((auth_url, state)) + } + + /// Validate and consume a state parameter, returning the pending flow + /// + /// This is called during the OAuth callback to verify CSRF protection + pub fn validate_state(&self, state: &str) -> Result { + let (_, pending) = self + .pending_flows + .remove(state) + .ok_or_else(|| anyhow!("Invalid or expired OAuth state parameter"))?; + + // Check TTL + let age = Utc::now().signed_duration_since(pending.created_at); + if age > Duration::seconds(OAUTH_STATE_TTL_SECS) { + warn!( + plugin_id = %pending.plugin_id, + user_id = %pending.user_id, + age_secs = age.num_seconds(), + "OAuth state expired" + ); + return Err(anyhow!( + "OAuth state expired ({}s > {}s)", + age.num_seconds(), + OAUTH_STATE_TTL_SECS + )); + } + + Ok(pending) + } + + /// Exchange an authorization code for tokens + pub async fn exchange_code( + &self, + oauth_config: &OAuthConfig, + code: &str, + client_id: &str, + client_secret: Option<&str>, + redirect_uri: &str, + pkce_verifier: Option<&str>, + ) -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?; + + let mut params = vec![ + ("grant_type", "authorization_code"), + ("code", code), + ("client_id", client_id), + ("redirect_uri", redirect_uri), + ]; + + // Add client_secret if present + let secret_string; + if let Some(secret) = client_secret { + secret_string = secret.to_string(); + params.push(("client_secret", &secret_string)); + } + + // Add PKCE verifier if present + let verifier_string; + if let Some(verifier) = pkce_verifier { + verifier_string = verifier.to_string(); + params.push(("code_verifier", &verifier_string)); + } + + let response = client + .post(&oauth_config.token_url) + .form(¶ms) + .send() + .await + .map_err(|e| anyhow!("Token exchange HTTP request failed: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response + .text() + .await + .unwrap_or_else(|_| "Unable to read response body".to_string()); + return Err(anyhow!( + "Token exchange failed with status {}: {}", + status, + body + )); + } + + let token_response: OAuthTokenResponse = response + .json() + .await + .map_err(|e| anyhow!("Failed to parse token response: {}", e))?; + + let expires_at = token_response + .expires_in + .map(|secs| Utc::now() + Duration::seconds(secs as i64)); + + Ok(OAuthResult { + access_token: token_response.access_token, + refresh_token: token_response.refresh_token, + expires_at, + scope: token_response.scope, + }) + } + + /// Clean up expired pending flows + pub fn cleanup_expired(&self) -> usize { + let now = Utc::now(); + let ttl = Duration::seconds(OAUTH_STATE_TTL_SECS); + let mut removed = 0; + + self.pending_flows.retain(|_, flow| { + let expired = now.signed_duration_since(flow.created_at) > ttl; + if expired { + removed += 1; + } + !expired + }); + + if removed > 0 { + debug!(removed, "Cleaned up expired OAuth flows"); + } + + removed + } + + /// Get the total number of pending flows (used in tests and monitoring) + pub fn pending_count(&self) -> usize { + self.pending_flows.len() + } + + /// Get the number of pending flows for a specific user (for rate-limiting) + pub fn pending_count_for_user(&self, user_id: Uuid) -> usize { + self.pending_flows + .iter() + .filter(|entry| entry.value().user_id == user_id) + .count() + } +} + +impl Default for OAuthStateManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_oauth_config() -> OAuthConfig { + OAuthConfig { + authorization_url: "https://example.com/oauth/authorize".to_string(), + token_url: "https://example.com/oauth/token".to_string(), + scopes: vec!["read".to_string(), "write".to_string()], + pkce: true, + user_info_url: None, + client_id: None, + } + } + + #[test] + fn test_generate_state() { + let state1 = OAuthStateManager::generate_state(); + let state2 = OAuthStateManager::generate_state(); + + // States should be non-empty + assert!(!state1.is_empty()); + assert!(!state2.is_empty()); + + // States should be different + assert_ne!(state1, state2); + + // Should be base64url encoded (43 chars for 32 bytes) + assert_eq!(state1.len(), 43); + } + + #[test] + fn test_generate_pkce() { + let (verifier, challenge) = OAuthStateManager::generate_pkce(); + + // Both should be non-empty + assert!(!verifier.is_empty()); + assert!(!challenge.is_empty()); + + // Verifier should be base64url encoded (43 chars for 32 bytes) + assert_eq!(verifier.len(), 43); + + // Challenge should be base64url encoded SHA256 (43 chars for 32 bytes) + assert_eq!(challenge.len(), 43); + + // Challenge should be deterministic for a given verifier + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(verifier.as_bytes()); + let expected_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize()); + assert_eq!(challenge, expected_challenge); + } + + #[test] + fn test_start_oauth_flow() { + let manager = OAuthStateManager::new(); + let config = test_oauth_config(); + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + let (auth_url, state) = manager + .start_oauth_flow( + plugin_id, + user_id, + &config, + "my-client-id", + "https://codex.local/api/v1/user/plugins/oauth/callback", + ) + .unwrap(); + + // Auth URL should contain required parameters + assert!(auth_url.starts_with("https://example.com/oauth/authorize?")); + assert!(auth_url.contains("response_type=code")); + assert!(auth_url.contains("client_id=my-client-id")); + assert!(auth_url.contains("redirect_uri=")); + assert!(auth_url.contains("state=")); + assert!(auth_url.contains("scope=read") && auth_url.contains("write")); + assert!(auth_url.contains("code_challenge=")); + assert!(auth_url.contains("code_challenge_method=S256")); + + // State should be stored + assert_eq!(manager.pending_count(), 1); + + // State should be non-empty + assert!(!state.is_empty()); + } + + #[test] + fn test_start_oauth_flow_without_pkce() { + let manager = OAuthStateManager::new(); + let mut config = test_oauth_config(); + config.pkce = false; + config.scopes = vec![]; + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + let (auth_url, _) = manager + .start_oauth_flow( + plugin_id, + user_id, + &config, + "my-client-id", + "https://codex.local/callback", + ) + .unwrap(); + + // Should NOT contain PKCE parameters + assert!(!auth_url.contains("code_challenge")); + assert!(!auth_url.contains("code_challenge_method")); + + // Should NOT contain scope parameter (empty scopes) + assert!(!auth_url.contains("scope=")); + } + + #[test] + fn test_validate_state_success() { + let manager = OAuthStateManager::new(); + let config = test_oauth_config(); + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + let (_, state) = manager + .start_oauth_flow( + plugin_id, + user_id, + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + // Validate should succeed + let pending = manager.validate_state(&state).unwrap(); + assert_eq!(pending.plugin_id, plugin_id); + assert_eq!(pending.user_id, user_id); + assert!(pending.pkce_verifier.is_some()); + + // State should be consumed (removed) + assert_eq!(manager.pending_count(), 0); + } + + #[test] + fn test_validate_state_invalid() { + let manager = OAuthStateManager::new(); + + // Should fail for unknown state + assert!(manager.validate_state("nonexistent").is_err()); + } + + #[test] + fn test_validate_state_consumed() { + let manager = OAuthStateManager::new(); + let config = test_oauth_config(); + + let (_, state) = manager + .start_oauth_flow( + Uuid::new_v4(), + Uuid::new_v4(), + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + // First validation should succeed + assert!(manager.validate_state(&state).is_ok()); + + // Second validation should fail (state consumed) + assert!(manager.validate_state(&state).is_err()); + } + + #[test] + fn test_cleanup_expired() { + let manager = OAuthStateManager::new(); + let config = test_oauth_config(); + + // Create a flow + manager + .start_oauth_flow( + Uuid::new_v4(), + Uuid::new_v4(), + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + assert_eq!(manager.pending_count(), 1); + + // Cleanup should not remove fresh flows + let removed = manager.cleanup_expired(); + assert_eq!(removed, 0); + assert_eq!(manager.pending_count(), 1); + } + + #[test] + fn test_multiple_flows() { + let manager = OAuthStateManager::new(); + let config = test_oauth_config(); + + // Start multiple flows + let (_, state1) = manager + .start_oauth_flow( + Uuid::new_v4(), + Uuid::new_v4(), + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + let (_, state2) = manager + .start_oauth_flow( + Uuid::new_v4(), + Uuid::new_v4(), + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + assert_eq!(manager.pending_count(), 2); + + // States should be different + assert_ne!(state1, state2); + + // Each should validate independently + assert!(manager.validate_state(&state1).is_ok()); + assert_eq!(manager.pending_count(), 1); + assert!(manager.validate_state(&state2).is_ok()); + assert_eq!(manager.pending_count(), 0); + } + + #[test] + fn test_pending_count_for_user() { + let manager = OAuthStateManager::new(); + let config = test_oauth_config(); + let user_a = Uuid::new_v4(); + let user_b = Uuid::new_v4(); + + // Start flows for user_a + manager + .start_oauth_flow( + Uuid::new_v4(), + user_a, + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + manager + .start_oauth_flow( + Uuid::new_v4(), + user_a, + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + // Start a flow for user_b + manager + .start_oauth_flow( + Uuid::new_v4(), + user_b, + &config, + "client-id", + "https://codex.local/callback", + ) + .unwrap(); + + assert_eq!(manager.pending_count(), 3); + assert_eq!(manager.pending_count_for_user(user_a), 2); + assert_eq!(manager.pending_count_for_user(user_b), 1); + assert_eq!(manager.pending_count_for_user(Uuid::new_v4()), 0); + } +} diff --git a/src/services/user_plugin/token_refresh.rs b/src/services/user_plugin/token_refresh.rs new file mode 100644 index 00000000..3f5b2401 --- /dev/null +++ b/src/services/user_plugin/token_refresh.rs @@ -0,0 +1,740 @@ +//! Token Refresh Service for User Plugins +//! +//! Handles automatic refresh of OAuth tokens before they expire. +//! Called before plugin operations to ensure valid tokens are available. + +use anyhow::{Result, anyhow}; +use chrono::{Duration, Utc}; +use sea_orm::DatabaseConnection; +use tracing::{debug, info, warn}; + +use crate::db::entities::user_plugins; +use crate::db::repositories::UserPluginsRepository; +use crate::services::plugin::protocol::OAuthConfig; + +use super::oauth::OAuthTokenResponse; + +/// Buffer time before expiry to trigger refresh (5 minutes) +const REFRESH_BUFFER_SECS: i64 = 300; + +/// Maximum consecutive failures before circuit breaker trips. +/// After this many failures within the time window, refresh attempts +/// are skipped and the user is immediately asked to re-authenticate. +pub const CIRCUIT_BREAKER_FAILURE_THRESHOLD: i32 = 3; + +/// Time window (in seconds) for the circuit breaker. +/// Only failures within this window count toward the threshold. +pub const CIRCUIT_BREAKER_WINDOW_SECS: i64 = 3600; // 1 hour + +/// Refresh result +#[derive(Debug)] +pub enum RefreshResult { + /// Token was refreshed successfully + Refreshed { access_token: String }, + /// Token is still valid, no refresh needed + StillValid, + /// No refresh token available, user needs to re-authenticate + ReauthRequired, + /// Refresh failed with an error + Failed(String), +} + +/// Structured error from an OAuth token refresh attempt. +/// +/// Classifies errors by their HTTP response and body content rather than +/// relying on fragile string matching. +#[derive(Debug)] +pub enum TokenRefreshError { + /// The refresh token is invalid or revoked — user must re-authenticate. + /// Triggered by HTTP 401, or HTTP 400 with `error` in + /// `["invalid_grant", "invalid_client", "unauthorized_client"]`. + ReauthRequired { + status: u16, + error_code: Option, + description: Option, + }, + /// Rate limited by the OAuth provider — retry later. + RateLimited { + status: u16, + retry_after: Option, + }, + /// Temporary failure — may succeed on retry. + Temporary { + status: Option, + message: String, + }, + /// Network or transport error (no HTTP response). + Network(String), +} + +impl std::fmt::Display for TokenRefreshError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TokenRefreshError::ReauthRequired { + status, + error_code, + description, + } => { + write!(f, "Re-authentication required (HTTP {})", status)?; + if let Some(code) = error_code { + write!(f, ": {}", code)?; + } + if let Some(desc) = description { + write!(f, " — {}", desc)?; + } + Ok(()) + } + TokenRefreshError::RateLimited { + status, + retry_after, + } => { + write!(f, "Rate limited (HTTP {})", status)?; + if let Some(after) = retry_after { + write!(f, ", retry after: {}", after)?; + } + Ok(()) + } + TokenRefreshError::Temporary { status, message } => { + if let Some(s) = status { + write!(f, "Token refresh failed (HTTP {}): {}", s, message) + } else { + write!(f, "Token refresh failed: {}", message) + } + } + TokenRefreshError::Network(msg) => { + write!(f, "Token refresh network error: {}", msg) + } + } + } +} + +impl std::error::Error for TokenRefreshError {} + +/// OAuth error response body as defined by RFC 6749 §5.2. +#[derive(Debug, serde::Deserialize)] +struct OAuthErrorBody { + #[serde(default)] + error: Option, + #[serde(default)] + error_description: Option, +} + +/// OAuth error codes that indicate the refresh token is permanently invalid +/// and the user must re-authenticate. +const REAUTH_ERROR_CODES: &[&str] = &["invalid_grant", "invalid_client", "unauthorized_client"]; + +/// Check if a user plugin's OAuth token needs refreshing and perform the refresh if needed. +/// +/// Returns `RefreshResult` indicating whether the token was refreshed, still valid, +/// or requires re-authentication. +pub async fn ensure_valid_token( + db: &DatabaseConnection, + user_plugin: &user_plugins::Model, + oauth_config: &OAuthConfig, + client_id: &str, + client_secret: Option<&str>, +) -> Result { + // Check if plugin has OAuth tokens + if !user_plugin.has_oauth_tokens() { + return Ok(RefreshResult::ReauthRequired); + } + + // Check if token is still valid (with buffer) + if !needs_refresh(user_plugin) { + return Ok(RefreshResult::StillValid); + } + + // Circuit breaker: skip refresh if too many recent failures + if is_circuit_open(user_plugin) { + warn!( + user_plugin_id = %user_plugin.id, + failure_count = user_plugin.failure_count, + last_failure_at = ?user_plugin.last_failure_at, + "Circuit breaker open: {} failures within window, requiring re-authentication", + user_plugin.failure_count + ); + return Ok(RefreshResult::ReauthRequired); + } + + debug!( + user_plugin_id = %user_plugin.id, + expires_at = ?user_plugin.oauth_expires_at, + "OAuth token needs refresh" + ); + + // Get the encrypted refresh token + let refresh_token = + match UserPluginsRepository::get_oauth_refresh_token(db, user_plugin.id).await { + Ok(Some(token)) => token, + Ok(None) => { + warn!( + user_plugin_id = %user_plugin.id, + "No refresh token available, re-authentication required" + ); + return Ok(RefreshResult::ReauthRequired); + } + Err(e) => { + return Ok(RefreshResult::Failed(format!( + "Failed to get refresh token: {}", + e + ))); + } + }; + + // Perform the token refresh + match refresh_oauth_token(oauth_config, &refresh_token, client_id, client_secret).await { + Ok(token_response) => { + let expires_at = token_response + .expires_in + .map(|secs| Utc::now() + Duration::seconds(secs as i64)); + + // Store new tokens + UserPluginsRepository::update_oauth_tokens( + db, + user_plugin.id, + &token_response.access_token, + token_response.refresh_token.as_deref(), + expires_at, + token_response.scope.as_deref(), + ) + .await + .map_err(|e| anyhow!("Failed to store refreshed tokens: {}", e))?; + + // Record success + let _ = UserPluginsRepository::record_success(db, user_plugin.id).await; + + info!( + user_plugin_id = %user_plugin.id, + expires_at = ?expires_at, + "Successfully refreshed OAuth token" + ); + + Ok(RefreshResult::Refreshed { + access_token: token_response.access_token, + }) + } + Err(refresh_err) => { + warn!( + user_plugin_id = %user_plugin.id, + error = %refresh_err, + "OAuth token refresh failed" + ); + + // Record failure + let _ = UserPluginsRepository::record_failure(db, user_plugin.id).await; + + // Classify the error using structured matching + match refresh_err { + TokenRefreshError::ReauthRequired { .. } => Ok(RefreshResult::ReauthRequired), + TokenRefreshError::RateLimited { .. } => { + Ok(RefreshResult::Failed(refresh_err.to_string())) + } + TokenRefreshError::Temporary { .. } | TokenRefreshError::Network(..) => { + Ok(RefreshResult::Failed(refresh_err.to_string())) + } + } + } + } +} + +/// Check if the circuit breaker is open (too many recent failures). +/// +/// Returns `true` if the user plugin has >= `CIRCUIT_BREAKER_FAILURE_THRESHOLD` +/// failures within the last `CIRCUIT_BREAKER_WINDOW_SECS`. +fn is_circuit_open(user_plugin: &user_plugins::Model) -> bool { + if user_plugin.failure_count < CIRCUIT_BREAKER_FAILURE_THRESHOLD { + return false; + } + + // Check if the failures are within the time window + match user_plugin.last_failure_at { + Some(last_failure) => { + let window = Duration::seconds(CIRCUIT_BREAKER_WINDOW_SECS); + let cutoff = Utc::now() - window; + last_failure >= cutoff + } + // No timestamp but high failure count — treat as open + None => true, + } +} + +/// Check if the token needs refreshing based on expiry time +fn needs_refresh(user_plugin: &user_plugins::Model) -> bool { + match user_plugin.oauth_expires_at { + Some(expires_at) => { + let buffer = Duration::seconds(REFRESH_BUFFER_SECS); + Utc::now() + buffer >= expires_at + } + // No expiry set - assume token doesn't expire + None => false, + } +} + +/// Classify an HTTP error response into a structured `TokenRefreshError`. +/// +/// This replaces string matching with proper HTTP status code and +/// OAuth error body parsing. +fn classify_http_error(status: u16, body: &str) -> TokenRefreshError { + // HTTP 401 always means re-auth required + if status == 401 { + let parsed = serde_json::from_str::(body).ok(); + return TokenRefreshError::ReauthRequired { + status, + error_code: parsed.as_ref().and_then(|b| b.error.clone()), + description: parsed.as_ref().and_then(|b| b.error_description.clone()), + }; + } + + // HTTP 429 = rate limited + if status == 429 { + return TokenRefreshError::RateLimited { + status, + retry_after: None, + }; + } + + // HTTP 400 — check for OAuth error codes that indicate permanent failure + if status == 400 + && let Ok(error_body) = serde_json::from_str::(body) + && let Some(ref error_code) = error_body.error + && REAUTH_ERROR_CODES.contains(&error_code.as_str()) + { + return TokenRefreshError::ReauthRequired { + status, + error_code: Some(error_code.clone()), + description: error_body.error_description, + }; + } + + // All other errors are treated as temporary + TokenRefreshError::Temporary { + status: Some(status), + message: if body.is_empty() { + format!("HTTP {}", status) + } else { + format!("HTTP {}: {}", status, body) + }, + } +} + +/// Perform the actual token refresh HTTP request +async fn refresh_oauth_token( + oauth_config: &OAuthConfig, + refresh_token: &str, + client_id: &str, + client_secret: Option<&str>, +) -> std::result::Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| TokenRefreshError::Network(format!("Failed to create HTTP client: {}", e)))?; + + let mut params = vec![ + ("grant_type", "refresh_token"), + ("refresh_token", refresh_token), + ("client_id", client_id), + ]; + + let secret_string; + if let Some(secret) = client_secret { + secret_string = secret.to_string(); + params.push(("client_secret", &secret_string)); + } + + let response = client + .post(&oauth_config.token_url) + .form(¶ms) + .send() + .await + .map_err(|e| { + TokenRefreshError::Network(format!("Token refresh HTTP request failed: {}", e)) + })?; + + if !response.status().is_success() { + let status = response.status().as_u16(); + let body = response + .text() + .await + .unwrap_or_else(|_| "Unable to read response body".to_string()); + return Err(classify_http_error(status, &body)); + } + + let token_response: OAuthTokenResponse = + response + .json() + .await + .map_err(|e| TokenRefreshError::Temporary { + status: None, + message: format!("Failed to parse token refresh response: {}", e), + })?; + + Ok(token_response) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + use uuid::Uuid; + + fn create_test_user_plugin( + expires_at: Option>, + has_tokens: bool, + ) -> user_plugins::Model { + user_plugins::Model { + id: Uuid::new_v4(), + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + credentials: None, + config: serde_json::json!({}), + oauth_access_token: if has_tokens { + Some(vec![1, 2, 3]) + } else { + None + }, + oauth_refresh_token: if has_tokens { + Some(vec![4, 5, 6]) + } else { + None + }, + oauth_expires_at: expires_at, + oauth_scope: None, + external_user_id: None, + external_username: None, + external_avatar_url: None, + enabled: true, + health_status: "healthy".to_string(), + failure_count: 0, + last_failure_at: None, + last_success_at: None, + last_sync_at: None, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + + fn create_test_user_plugin_with_failures( + expires_at: Option>, + failure_count: i32, + last_failure_at: Option>, + ) -> user_plugins::Model { + user_plugins::Model { + failure_count, + last_failure_at, + ..create_test_user_plugin(expires_at, true) + } + } + + // ========================================================================= + // needs_refresh tests + // ========================================================================= + + #[test] + fn test_needs_refresh_no_expiry() { + let user_plugin = create_test_user_plugin(None, true); + assert!(!needs_refresh(&user_plugin)); + } + + #[test] + fn test_needs_refresh_far_future() { + let user_plugin = create_test_user_plugin(Some(Utc::now() + Duration::hours(1)), true); + assert!(!needs_refresh(&user_plugin)); + } + + #[test] + fn test_needs_refresh_within_buffer() { + // Token expires in 3 minutes, buffer is 5 minutes → needs refresh + let user_plugin = create_test_user_plugin(Some(Utc::now() + Duration::minutes(3)), true); + assert!(needs_refresh(&user_plugin)); + } + + #[test] + fn test_needs_refresh_already_expired() { + let user_plugin = create_test_user_plugin(Some(Utc::now() - Duration::minutes(5)), true); + assert!(needs_refresh(&user_plugin)); + } + + #[test] + fn test_needs_refresh_at_boundary() { + // Token expires in exactly REFRESH_BUFFER_SECS → needs refresh + let user_plugin = create_test_user_plugin( + Some(Utc::now() + Duration::seconds(REFRESH_BUFFER_SECS)), + true, + ); + assert!(needs_refresh(&user_plugin)); + } + + // ========================================================================= + // classify_http_error tests + // ========================================================================= + + #[test] + fn test_classify_http_401_reauth_required() { + let err = classify_http_error(401, r#"{"error":"invalid_token"}"#); + match err { + TokenRefreshError::ReauthRequired { + status, error_code, .. + } => { + assert_eq!(status, 401); + assert_eq!(error_code.as_deref(), Some("invalid_token")); + } + other => panic!("Expected ReauthRequired, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_401_no_body() { + let err = classify_http_error(401, ""); + match err { + TokenRefreshError::ReauthRequired { + status, + error_code, + description, + } => { + assert_eq!(status, 401); + assert!(error_code.is_none()); + assert!(description.is_none()); + } + other => panic!("Expected ReauthRequired, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_401_non_json_body() { + let err = classify_http_error(401, "Unauthorized"); + match err { + TokenRefreshError::ReauthRequired { + status, error_code, .. + } => { + assert_eq!(status, 401); + assert!(error_code.is_none()); + } + other => panic!("Expected ReauthRequired, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_400_invalid_grant() { + let err = classify_http_error( + 400, + r#"{"error":"invalid_grant","error_description":"Token has been revoked"}"#, + ); + match err { + TokenRefreshError::ReauthRequired { + status, + error_code, + description, + } => { + assert_eq!(status, 400); + assert_eq!(error_code.as_deref(), Some("invalid_grant")); + assert_eq!(description.as_deref(), Some("Token has been revoked")); + } + other => panic!("Expected ReauthRequired, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_400_invalid_client() { + let err = classify_http_error( + 400, + r#"{"error":"invalid_client","error_description":"Client not found"}"#, + ); + match err { + TokenRefreshError::ReauthRequired { + status, error_code, .. + } => { + assert_eq!(status, 400); + assert_eq!(error_code.as_deref(), Some("invalid_client")); + } + other => panic!("Expected ReauthRequired, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_400_unauthorized_client() { + let err = classify_http_error(400, r#"{"error":"unauthorized_client"}"#); + match err { + TokenRefreshError::ReauthRequired { + status, error_code, .. + } => { + assert_eq!(status, 400); + assert_eq!(error_code.as_deref(), Some("unauthorized_client")); + } + other => panic!("Expected ReauthRequired, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_400_other_error_is_temporary() { + let err = classify_http_error( + 400, + r#"{"error":"invalid_request","error_description":"Missing parameter"}"#, + ); + match err { + TokenRefreshError::Temporary { status, message } => { + assert_eq!(status, Some(400)); + assert!(message.contains("invalid_request")); + } + other => panic!("Expected Temporary, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_400_non_json_body() { + let err = classify_http_error(400, "Bad Request"); + match err { + TokenRefreshError::Temporary { status, message } => { + assert_eq!(status, Some(400)); + assert!(message.contains("Bad Request")); + } + other => panic!("Expected Temporary, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_429_rate_limited() { + let err = classify_http_error(429, "Too Many Requests"); + match err { + TokenRefreshError::RateLimited { status, .. } => { + assert_eq!(status, 429); + } + other => panic!("Expected RateLimited, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_500_temporary() { + let err = classify_http_error(500, "Internal Server Error"); + match err { + TokenRefreshError::Temporary { status, message } => { + assert_eq!(status, Some(500)); + assert!(message.contains("Internal Server Error")); + } + other => panic!("Expected Temporary, got: {:?}", other), + } + } + + #[test] + fn test_classify_http_503_temporary() { + let err = classify_http_error(503, ""); + match err { + TokenRefreshError::Temporary { status, message } => { + assert_eq!(status, Some(503)); + assert_eq!(message, "HTTP 503"); + } + other => panic!("Expected Temporary, got: {:?}", other), + } + } + + // ========================================================================= + // circuit breaker tests + // ========================================================================= + + #[test] + fn test_circuit_open_below_threshold() { + let user_plugin = create_test_user_plugin_with_failures( + Some(Utc::now() + Duration::minutes(3)), + 2, + Some(Utc::now()), + ); + assert!(!is_circuit_open(&user_plugin)); + } + + #[test] + fn test_circuit_open_at_threshold_recent_failure() { + let user_plugin = create_test_user_plugin_with_failures( + Some(Utc::now() + Duration::minutes(3)), + CIRCUIT_BREAKER_FAILURE_THRESHOLD, + Some(Utc::now()), + ); + assert!(is_circuit_open(&user_plugin)); + } + + #[test] + fn test_circuit_open_above_threshold_recent_failure() { + let user_plugin = create_test_user_plugin_with_failures( + Some(Utc::now() + Duration::minutes(3)), + CIRCUIT_BREAKER_FAILURE_THRESHOLD + 5, + Some(Utc::now()), + ); + assert!(is_circuit_open(&user_plugin)); + } + + #[test] + fn test_circuit_closed_old_failures() { + // Failures happened 2 hours ago — outside the 1-hour window + let user_plugin = create_test_user_plugin_with_failures( + Some(Utc::now() + Duration::minutes(3)), + CIRCUIT_BREAKER_FAILURE_THRESHOLD + 1, + Some(Utc::now() - Duration::hours(2)), + ); + assert!(!is_circuit_open(&user_plugin)); + } + + #[test] + fn test_circuit_open_no_timestamp_high_count() { + // High failure count but no timestamp — treat as open + let user_plugin = create_test_user_plugin_with_failures( + Some(Utc::now() + Duration::minutes(3)), + CIRCUIT_BREAKER_FAILURE_THRESHOLD, + None, + ); + assert!(is_circuit_open(&user_plugin)); + } + + #[test] + fn test_circuit_closed_zero_failures() { + let user_plugin = + create_test_user_plugin_with_failures(Some(Utc::now() + Duration::minutes(3)), 0, None); + assert!(!is_circuit_open(&user_plugin)); + } + + // ========================================================================= + // TokenRefreshError Display tests + // ========================================================================= + + #[test] + fn test_display_reauth_required() { + let err = TokenRefreshError::ReauthRequired { + status: 400, + error_code: Some("invalid_grant".to_string()), + description: Some("Token revoked".to_string()), + }; + let s = err.to_string(); + assert!(s.contains("Re-authentication required")); + assert!(s.contains("400")); + assert!(s.contains("invalid_grant")); + assert!(s.contains("Token revoked")); + } + + #[test] + fn test_display_rate_limited() { + let err = TokenRefreshError::RateLimited { + status: 429, + retry_after: Some("60".to_string()), + }; + let s = err.to_string(); + assert!(s.contains("Rate limited")); + assert!(s.contains("429")); + assert!(s.contains("retry after: 60")); + } + + #[test] + fn test_display_temporary() { + let err = TokenRefreshError::Temporary { + status: Some(500), + message: "Internal error".to_string(), + }; + let s = err.to_string(); + assert!(s.contains("500")); + assert!(s.contains("Internal error")); + } + + #[test] + fn test_display_network() { + let err = TokenRefreshError::Network("connection refused".to_string()); + let s = err.to_string(); + assert!(s.contains("network error")); + assert!(s.contains("connection refused")); + } +} diff --git a/src/tasks/handlers/cleanup_plugin_data.rs b/src/tasks/handlers/cleanup_plugin_data.rs new file mode 100644 index 00000000..4676b0a6 --- /dev/null +++ b/src/tasks/handlers/cleanup_plugin_data.rs @@ -0,0 +1,137 @@ +//! Handler for CleanupPluginData task +//! +//! Periodically cleans up expired key-value data from plugin storage +//! (`user_plugin_data` table). Entries with a past `expires_at` timestamp +//! are deleted in bulk. Also cleans up expired OAuth state flows from the +//! in-memory `OAuthStateManager` to prevent memory leaks. + +use anyhow::Result; +use sea_orm::DatabaseConnection; +use serde_json::json; +use std::sync::Arc; +use tracing::info; + +use crate::db::entities::tasks; +use crate::db::repositories::UserPluginDataRepository; +use crate::events::EventBroadcaster; +use crate::services::user_plugin::OAuthStateManager; +use crate::tasks::handlers::TaskHandler; +use crate::tasks::types::TaskResult; + +/// Handler for cleaning up expired plugin storage data and OAuth state +#[derive(Default)] +pub struct CleanupPluginDataHandler { + oauth_state_manager: Option>, +} + +impl CleanupPluginDataHandler { + pub fn new() -> Self { + Self::default() + } + + /// Set the OAuth state manager for cleaning up expired OAuth flows + pub fn with_oauth_state_manager(mut self, manager: Arc) -> Self { + self.oauth_state_manager = Some(manager); + self + } +} + +impl TaskHandler for CleanupPluginDataHandler { + fn handle<'a>( + &'a self, + task: &'a tasks::Model, + db: &'a DatabaseConnection, + _event_broadcaster: Option<&'a Arc>, + ) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + info!("Task {}: Starting plugin data cleanup", task.id); + + let deleted_count = UserPluginDataRepository::cleanup_expired(db).await?; + + // Clean up expired OAuth pending flows from in-memory state + let (oauth_cleaned, oauth_remaining) = + if let Some(ref manager) = self.oauth_state_manager { + let cleaned = manager.cleanup_expired(); + let remaining = manager.pending_count(); + (cleaned, remaining) + } else { + (0, 0) + }; + + info!( + "Task {}: Plugin data cleanup complete - deleted {} expired storage entries, \ + {} expired OAuth flows ({} still pending)", + task.id, deleted_count, oauth_cleaned, oauth_remaining + ); + + Ok(TaskResult::success_with_data( + format!( + "Cleaned up {} expired plugin data entries, {} expired OAuth flows", + deleted_count, oauth_cleaned + ), + json!({ + "deleted_count": deleted_count, + "oauth_flows_cleaned": oauth_cleaned, + "oauth_flows_remaining": oauth_remaining, + }), + )) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::services::plugin::protocol::OAuthConfig; + use uuid::Uuid; + + #[test] + fn test_handler_creation() { + let _handler = CleanupPluginDataHandler::new(); + } + + #[test] + fn test_handler_with_oauth_state_manager() { + let manager = Arc::new(OAuthStateManager::new()); + let handler = CleanupPluginDataHandler::new().with_oauth_state_manager(manager.clone()); + assert!(handler.oauth_state_manager.is_some()); + } + + #[test] + fn test_cleanup_expired_oauth_flows() { + let manager = Arc::new(OAuthStateManager::new()); + let config = OAuthConfig { + authorization_url: "https://example.com/auth".to_string(), + token_url: "https://example.com/token".to_string(), + scopes: vec![], + pkce: false, + user_info_url: None, + client_id: None, + }; + + // Create a fresh flow (should NOT be cleaned up) + manager + .start_oauth_flow( + Uuid::new_v4(), + Uuid::new_v4(), + &config, + "client-id", + "https://example.com/callback", + ) + .unwrap(); + + assert_eq!(manager.pending_count(), 1); + + // Cleanup should not remove fresh flows + let removed = manager.cleanup_expired(); + assert_eq!(removed, 0); + assert_eq!(manager.pending_count(), 1); + } + + #[test] + fn test_handler_without_oauth_manager_still_works() { + // Handler without OAuthStateManager should work fine (no-op for OAuth cleanup) + let handler = CleanupPluginDataHandler::new(); + assert!(handler.oauth_state_manager.is_none()); + } +} diff --git a/src/tasks/handlers/mod.rs b/src/tasks/handlers/mod.rs index eae4d92e..5c89c574 100644 --- a/src/tasks/handlers/mod.rs +++ b/src/tasks/handlers/mod.rs @@ -11,6 +11,7 @@ pub mod analyze_series; pub mod cleanup_book_files; pub mod cleanup_orphaned_files; pub mod cleanup_pdf_cache; +pub mod cleanup_plugin_data; pub mod cleanup_series_files; pub mod find_duplicates; pub mod generate_series_thumbnail; @@ -21,12 +22,15 @@ pub mod plugin_auto_match; pub mod purge_deleted; pub mod reprocess_series_titles; pub mod scan_library; +pub mod user_plugin_recommendations; +pub mod user_plugin_sync; pub use analyze_book::AnalyzeBookHandler; pub use analyze_series::AnalyzeSeriesHandler; pub use cleanup_book_files::CleanupBookFilesHandler; pub use cleanup_orphaned_files::CleanupOrphanedFilesHandler; pub use cleanup_pdf_cache::CleanupPdfCacheHandler; +pub use cleanup_plugin_data::CleanupPluginDataHandler; pub use cleanup_series_files::CleanupSeriesFilesHandler; pub use find_duplicates::FindDuplicatesHandler; pub use generate_series_thumbnail::GenerateSeriesThumbnailHandler; @@ -37,6 +41,8 @@ pub use plugin_auto_match::PluginAutoMatchHandler; pub use purge_deleted::PurgeDeletedHandler; pub use reprocess_series_titles::{ReprocessSeriesTitleHandler, ReprocessSeriesTitlesHandler}; pub use scan_library::ScanLibraryHandler; +pub use user_plugin_recommendations::UserPluginRecommendationsHandler; +pub use user_plugin_sync::UserPluginSyncHandler; use std::future::Future; use std::pin::Pin; diff --git a/src/tasks/handlers/user_plugin_recommendations.rs b/src/tasks/handlers/user_plugin_recommendations.rs new file mode 100644 index 00000000..7704b44d --- /dev/null +++ b/src/tasks/handlers/user_plugin_recommendations.rs @@ -0,0 +1,149 @@ +//! Handler for UserPluginRecommendations task +//! +//! Processes recommendation refresh tasks by spawning the plugin process +//! with per-user credentials. Optionally calls `recommendations/clear` to +//! invalidate cached recommendations (if supported), then calls +//! `recommendations/get` to pre-generate fresh results. + +use anyhow::Result; +use sea_orm::DatabaseConnection; +use serde_json::json; +use std::sync::Arc; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +use crate::db::entities::tasks; +use crate::events::EventBroadcaster; +use crate::services::plugin::PluginManager; +use crate::services::plugin::library::build_user_library; +use crate::services::plugin::protocol::methods; +use crate::services::plugin::recommendations::{ + RecommendationClearResponse, RecommendationRequest, RecommendationResponse, +}; +use crate::tasks::handlers::TaskHandler; +use crate::tasks::types::TaskResult; + +/// Handler for user plugin recommendation refresh tasks +pub struct UserPluginRecommendationsHandler { + plugin_manager: Arc, +} + +impl UserPluginRecommendationsHandler { + pub fn new(plugin_manager: Arc) -> Self { + Self { plugin_manager } + } +} + +impl TaskHandler for UserPluginRecommendationsHandler { + fn handle<'a>( + &'a self, + task: &'a tasks::Model, + db: &'a DatabaseConnection, + _event_broadcaster: Option<&'a Arc>, + ) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + // Extract task parameters + let params = task.params.as_ref().ok_or_else(|| { + anyhow::anyhow!("Missing params in user_plugin_recommendations task") + })?; + + let plugin_id: Uuid = params + .get("plugin_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) + .ok_or_else(|| anyhow::anyhow!("Missing or invalid plugin_id in params"))?; + + let user_id: Uuid = params + .get("user_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) + .ok_or_else(|| anyhow::anyhow!("Missing or invalid user_id in params"))?; + + info!( + "Task {}: Refreshing recommendations for plugin {} / user {}", + task.id, plugin_id, user_id + ); + + // Get user plugin handle (spawns process with per-user credentials) + let (handle, _context) = self + .plugin_manager + .get_user_plugin_handle(plugin_id, user_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to spawn recommendation plugin: {}", e))?; + + // Try to clear cached recommendations (optional — not all plugins support this) + match handle + .call_method::( + methods::RECOMMENDATIONS_CLEAR, + json!({}), + ) + .await + { + Ok(response) => { + info!( + "Task {}: Recommendations cache cleared (cleared={})", + task.id, response.cleared + ); + } + Err(e) => { + debug!( + "Task {}: recommendations/clear not supported, skipping: {}", + task.id, e + ); + } + } + + // Build user library data to seed recommendations + let library = build_user_library(db, user_id).await.unwrap_or_else(|e| { + warn!( + "Task {}: Failed to build user library, using empty: {}", + task.id, e + ); + vec![] + }); + + debug!( + "Task {}: Sending {} library entries to recommendation plugin", + task.id, + library.len() + ); + + // Call recommendations/get to generate fresh results + let request = RecommendationRequest { + library, + limit: Some(20), + exclude_ids: vec![], + }; + + let response = handle + .call_method::( + methods::RECOMMENDATIONS_GET, + request, + ) + .await + .map_err(|e| { + warn!( + "Task {}: Failed to generate recommendations: {}", + task.id, e + ); + anyhow::anyhow!("Failed to generate recommendations: {}", e) + })?; + + let count = response.recommendations.len(); + info!( + "Task {}: Generated {} fresh recommendations for plugin {} / user {}", + task.id, count, plugin_id, user_id + ); + + Ok(TaskResult { + success: true, + message: Some(format!("Generated {} recommendations", count)), + data: Some(json!({ + "plugin_id": plugin_id.to_string(), + "user_id": user_id.to_string(), + "recommendation_count": count, + })), + }) + }) + } +} diff --git a/src/tasks/handlers/user_plugin_sync/mod.rs b/src/tasks/handlers/user_plugin_sync/mod.rs new file mode 100644 index 00000000..f7c2a9e9 --- /dev/null +++ b/src/tasks/handlers/user_plugin_sync/mod.rs @@ -0,0 +1,382 @@ +//! Handler for UserPluginSync task +//! +//! Processes user plugin sync tasks by spawning the plugin process with +//! per-user credentials and calling sync methods (push/pull progress) +//! via JSON-RPC. +//! +//! Module structure: +//! - `settings` — CodexSyncSettings parsing from user config +//! - `push` — Build entries from local reading progress to push +//! - `pull` — Match external entries and apply reading progress + +mod pull; +mod push; +pub(crate) mod settings; + +#[cfg(test)] +mod tests; + +use anyhow::Result; +use sea_orm::DatabaseConnection; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::Arc; +use tracing::{debug, error, info, warn}; +use uuid::Uuid; + +use crate::db::entities::tasks; +use crate::db::repositories::{UserPluginDataRepository, UserPluginsRepository}; +use crate::events::EventBroadcaster; +use crate::services::plugin::PluginManager; +use crate::services::plugin::protocol::methods; +use crate::services::plugin::sync::{ + ExternalUserInfo, SyncPullRequest, SyncPullResponse, SyncPushRequest, SyncPushResponse, +}; +use crate::tasks::handlers::TaskHandler; +use crate::tasks::types::TaskResult; + +pub(crate) use settings::CodexSyncSettings; + +/// Storage key under which the last sync result is persisted in `user_plugin_data`. +/// Used by the sync handler to write the result and by API handlers to read it. +pub const LAST_SYNC_RESULT_KEY: &str = "last_sync_result"; + +/// Result of a user plugin sync operation +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserPluginSyncResult { + /// Plugin ID + pub plugin_id: Uuid, + /// User ID + pub user_id: Uuid, + /// External username (if retrieved) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub external_username: Option, + /// Number of entries pushed + pub pushed: u32, + /// Number of entries pulled + pub pulled: u32, + /// Number of pulled entries matched to Codex series via external IDs + #[serde(default)] + pub matched: u32, + /// Number of books whose reading progress was applied from pulled entries + #[serde(default)] + pub applied: u32, + /// Push failures + pub push_failures: u32, + /// Pull had more pages (not all pulled) + #[serde(default)] + pub pull_incomplete: bool, + /// Error message if pull failed entirely + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pull_error: Option, + /// Error message if push failed entirely + #[serde(default, skip_serializing_if = "Option::is_none")] + pub push_error: Option, + /// Reason for skipping, if sync was skipped + #[serde(default, skip_serializing_if = "Option::is_none")] + pub skipped_reason: Option, +} + +/// Handler for user plugin sync tasks +pub struct UserPluginSyncHandler { + plugin_manager: Arc, +} + +impl UserPluginSyncHandler { + pub fn new(plugin_manager: Arc) -> Self { + Self { plugin_manager } + } +} + +impl TaskHandler for UserPluginSyncHandler { + fn handle<'a>( + &'a self, + task: &'a tasks::Model, + db: &'a DatabaseConnection, + _event_broadcaster: Option<&'a Arc>, + ) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + // Extract task parameters + let params = task + .params + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing params in user_plugin_sync task"))?; + + let plugin_id: Uuid = params + .get("plugin_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) + .ok_or_else(|| anyhow::anyhow!("Missing or invalid plugin_id in params"))?; + + let user_id: Uuid = params + .get("user_id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()) + .ok_or_else(|| anyhow::anyhow!("Missing or invalid user_id in params"))?; + + info!( + "Task {}: Starting sync for plugin {} / user {}", + task.id, plugin_id, user_id + ); + + // Read user plugin config + let user_config = + match UserPluginsRepository::get_by_user_and_plugin(db, user_id, plugin_id).await { + Ok(Some(instance)) => instance.config.clone(), + _ => serde_json::json!({}), + }; + let sync_mode = user_config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both") + .to_string(); + let do_pull = sync_mode == "both" || sync_mode == "pull"; + let do_push = sync_mode == "both" || sync_mode == "push"; + let codex_settings = CodexSyncSettings::from_user_config(&user_config); + + debug!( + "Task {}: syncMode={} (pull={}, push={})", + task.id, sync_mode, do_pull, do_push + ); + + // Get user plugin handle (spawns process with per-user credentials) + let (handle, context) = match self + .plugin_manager + .get_user_plugin_handle(plugin_id, user_id) + .await + { + Ok(result) => result, + Err(e) => { + let reason = match &e { + crate::services::plugin::PluginManagerError::UserPluginNotFound { + .. + } => "user_plugin_not_found", + crate::services::plugin::PluginManagerError::PluginNotEnabled(_) => { + "plugin_not_enabled" + } + _ => "plugin_start_failed", + }; + warn!("Task {}: Failed to get plugin handle: {}", task.id, e); + return Ok(TaskResult::success_with_data( + format!("Sync skipped: {}", reason), + json!(UserPluginSyncResult { + plugin_id, + user_id, + external_username: None, + pushed: 0, + pulled: 0, + matched: 0, + applied: 0, + push_failures: 0, + pull_incomplete: false, + pull_error: None, + push_error: None, + skipped_reason: Some(reason.to_string()), + }), + )); + } + }; + + // Step 1: Get external user info (optional, for display) + let external_username = match handle + .call_method::( + methods::SYNC_GET_USER_INFO, + json!({}), + ) + .await + { + Ok(user_info) => { + debug!( + "Task {}: Connected as '{}' ({})", + task.id, user_info.username, user_info.external_id + ); + Some(user_info.username) + } + Err(e) => { + warn!( + "Task {}: Failed to get user info (continuing): {}", + task.id, e + ); + None + } + }; + + // Resolve the external ID source from the plugin manifest + let external_id_source = handle + .manifest() + .await + .and_then(|m| m.capabilities.external_id_source.clone()); + + if let Some(ref source) = external_id_source { + debug!( + "Task {}: Plugin declares externalIdSource: {}", + task.id, source + ); + } + + // Step 2: Pull progress from external service + let (pulled_count, pull_incomplete, matched_count, applied_count, pull_error) = + if do_pull { + let pull_request = SyncPullRequest { + since: None, // Full pull for now; incremental can use last_sync_at + limit: Some(500), + cursor: None, + }; + + match handle + .call_method::( + methods::SYNC_PULL_PROGRESS, + pull_request, + ) + .await + { + Ok(pull_response) => { + let count = pull_response.entries.len() as u32; + let has_more = pull_response.has_more; + info!( + "Task {}: Pulled {} entries from external service (has_more: {})", + task.id, count, has_more + ); + + // Match pulled entries to Codex series and apply to reading progress + let (matched, applied) = pull::match_and_apply_pulled_entries( + db, + &pull_response.entries, + external_id_source.as_deref(), + user_id, + task.id, + codex_settings.sync_ratings, + ) + .await; + + if applied > 0 { + info!( + "Task {}: Applied reading progress for {} books", + task.id, applied + ); + } + + (count, has_more, matched, applied, None) + } + Err(e) => { + error!("Task {}: Pull failed: {}", task.id, e); + // Continue to push even if pull fails + (0, false, 0, 0, Some(e.to_string())) + } + } + } else { + info!("Task {}: Skipping pull (syncMode={})", task.id, sync_mode); + (0, false, 0, 0, None) + }; + + // Step 3: Push progress to external service + let (pushed_count, push_failures, push_error) = if do_push { + let entries = if let Some(ref source) = external_id_source { + push::build_push_entries(db, user_id, source, task.id, &codex_settings).await + } else { + warn!( + "Task {}: Plugin has no externalIdSource in manifest — cannot build push entries", + task.id + ); + vec![] + }; + info!( + "Task {}: Built {} push entries from reading progress", + task.id, + entries.len() + ); + let push_request = SyncPushRequest { entries }; + + match handle + .call_method::( + methods::SYNC_PUSH_PROGRESS, + push_request, + ) + .await + { + Ok(push_response) => { + let success_count = push_response.success.len() as u32; + let failure_count = push_response.failed.len() as u32; + if failure_count > 0 { + warn!( + "Task {}: Push had {} successes and {} failures", + task.id, success_count, failure_count + ); + } else { + info!( + "Task {}: Pushed {} entries to external service", + task.id, success_count + ); + } + (success_count, failure_count, None) + } + Err(e) => { + error!("Task {}: Push failed: {}", task.id, e); + (0, 0, Some(e.to_string())) + } + } + } else { + info!("Task {}: Skipping push (syncMode={})", task.id, sync_mode); + (0, 0, None) + }; + + let had_errors = pull_error.is_some() || push_error.is_some(); + + // Record sync timestamp on the user plugin instance + if let Err(e) = UserPluginsRepository::record_sync(db, context.user_plugin_id).await { + warn!("Task {}: Failed to record sync timestamp: {}", task.id, e); + } + + // Record success or failure on the user plugin instance + if had_errors { + if let Err(e) = + UserPluginsRepository::record_failure(db, context.user_plugin_id).await + { + warn!("Task {}: Failed to record failure: {}", task.id, e); + } + } else if let Err(e) = + UserPluginsRepository::record_success(db, context.user_plugin_id).await + { + warn!("Task {}: Failed to record success: {}", task.id, e); + } + + let result = UserPluginSyncResult { + plugin_id, + user_id, + external_username, + pushed: pushed_count, + pulled: pulled_count, + matched: matched_count, + applied: applied_count, + push_failures, + pull_incomplete, + pull_error, + push_error, + skipped_reason: None, + }; + + // Store sync result in user_plugin_data for display on the card + if let Err(e) = UserPluginDataRepository::set( + db, + context.user_plugin_id, + LAST_SYNC_RESULT_KEY, + json!(result), + None, + ) + .await + { + warn!("Task {}: Failed to store sync result: {}", task.id, e); + } + + let message = format!( + "Sync complete: pulled {} entries ({} matched, {} applied), pushed {} entries", + pulled_count, matched_count, applied_count, pushed_count + ); + + info!("Task {}: {}", task.id, message); + + Ok(TaskResult::success_with_data(message, json!(result))) + }) + } +} diff --git a/src/tasks/handlers/user_plugin_sync/pull.rs b/src/tasks/handlers/user_plugin_sync/pull.rs new file mode 100644 index 00000000..36bbe796 --- /dev/null +++ b/src/tasks/handlers/user_plugin_sync/pull.rs @@ -0,0 +1,245 @@ +//! Pull operations — match external entries to local series and apply +//! reading progress. + +use sea_orm::DatabaseConnection; +use std::collections::HashMap; +use tracing::{debug, warn}; +use uuid::Uuid; + +use crate::db::repositories::{ + BookRepository, ReadProgressRepository, SeriesExternalIdRepository, UserSeriesRatingRepository, +}; +use crate::services::plugin::sync::{SyncEntry, SyncReadingStatus}; + +/// Match pulled sync entries to Codex series using external IDs and apply +/// reading progress. +/// +/// For each pulled entry, looks up `series_external_ids` where +/// `source = external_id_source` and `external_id = entry.external_id`. +/// When a match is found, applies the pulled reading progress to the user's +/// Codex books (each book = 1 chapter). +/// +/// Returns `(matched, applied)` — matched entries count and books updated. +pub(crate) async fn match_and_apply_pulled_entries( + db: &DatabaseConnection, + entries: &[SyncEntry], + external_id_source: Option<&str>, + user_id: Uuid, + task_id: Uuid, + sync_ratings: bool, +) -> (u32, u32) { + let Some(source) = external_id_source else { + debug!( + "Task {}: No externalIdSource configured, skipping entry matching", + task_id + ); + return (0, 0); + }; + + if entries.is_empty() { + return (0, 0); + } + + // 1. Batch-fetch all external ID → series mappings (1 query instead of N) + let entry_external_ids: Vec = entries.iter().map(|e| e.external_id.clone()).collect(); + let ext_id_map = match SeriesExternalIdRepository::find_by_external_ids_and_source( + db, + &entry_external_ids, + source, + ) + .await + { + Ok(map) => map, + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch external IDs for source {}: {}", + task_id, source, e + ); + return (0, 0); + } + }; + + // 2. Batch-fetch books for all matched series (1 query instead of N) + let matched_series_ids: Vec = ext_id_map.values().map(|e| e.series_id).collect(); + let books_map = match BookRepository::get_by_series_ids(db, &matched_series_ids).await { + Ok(map) => map, + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch books for pull apply: {}", + task_id, e + ); + return (0, 0); + } + }; + + // 3. Batch-fetch reading progress for all books in matched series (1 query instead of N*M) + let all_book_ids: Vec = books_map.values().flatten().map(|b| b.id).collect(); + let progress_map = + match ReadProgressRepository::get_for_user_books(db, user_id, &all_book_ids).await { + Ok(map) => map, + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch reading progress for pull: {}", + task_id, e + ); + HashMap::new() + } + }; + + // 4. Batch-fetch existing ratings if sync_ratings is enabled (1 query instead of N) + let existing_ratings: HashMap = + if sync_ratings { + match UserSeriesRatingRepository::get_all_for_user(db, user_id).await { + Ok(ratings) => ratings.into_iter().map(|r| (r.series_id, r)).collect(), + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch existing ratings: {}", + task_id, e + ); + HashMap::new() + } + } + } else { + HashMap::new() + }; + + let mut matched: u32 = 0; + let mut unmatched: u32 = 0; + let mut applied: u32 = 0; + + for entry in entries { + match ext_id_map.get(&entry.external_id) { + Some(ext_id) => { + debug!( + "Task {}: Matched entry {} -> series {} (source: {})", + task_id, entry.external_id, ext_id.series_id, source + ); + matched += 1; + + // Apply reading progress using pre-fetched data + let books_applied = apply_pulled_entry( + db, + user_id, + ext_id.series_id, + entry, + task_id, + &books_map, + &progress_map, + ) + .await; + applied += books_applied; + + // Apply pulled rating/notes if enabled and Codex has no existing rating + if sync_ratings && let Some(pulled_score) = entry.score { + if !existing_ratings.contains_key(&ext_id.series_id) { + let score_i32 = (pulled_score.round() as i32).clamp(1, 100); + if let Err(e) = UserSeriesRatingRepository::upsert( + db, + user_id, + ext_id.series_id, + score_i32, + entry.notes.clone(), + ) + .await + { + warn!( + "Task {}: Failed to apply pulled rating for series {}: {}", + task_id, ext_id.series_id, e + ); + } + } else { + debug!( + "Task {}: Skipping pulled rating for series {} — Codex already has a rating", + task_id, ext_id.series_id + ); + } + } + } + None => { + unmatched += 1; + } + } + } + + if unmatched > 0 { + debug!( + "Task {}: {} entries matched, {} unmatched (source: {})", + task_id, matched, unmatched, source + ); + } + + (matched, applied) +} + +/// Apply a single pulled entry's reading progress to a Codex series. +/// +/// Maps chapters_read from the external service to books in the series: +/// - If status is Completed → mark ALL books as read +/// - Otherwise → mark the first `chapters_read` books as read +/// +/// Only marks books that aren't already completed. Returns the number of +/// books newly marked as read. +/// +/// Uses pre-fetched `books_map` and `progress_map` to avoid N+1 queries. +/// Only issues write queries (`mark_as_read`) for books that actually need updating. +async fn apply_pulled_entry( + db: &DatabaseConnection, + user_id: Uuid, + series_id: Uuid, + entry: &SyncEntry, + task_id: Uuid, + books_map: &HashMap>, + progress_map: &HashMap, +) -> u32 { + let books = match books_map.get(&series_id) { + Some(b) if !b.is_empty() => b, + _ => return 0, + }; + + // Use volumes if available, fall back to chapters + let units_read = entry + .progress + .as_ref() + .and_then(|p| p.volumes.or(p.chapters)) + .unwrap_or(0); + + // Determine which books to mark as read + let books_to_mark: &[crate::db::entities::books::Model] = + if entry.status == SyncReadingStatus::Completed { + // Mark all books as read + books + } else if units_read > 0 { + // Mark first N books as read (each book = 1 volume/chapter) + let n = (units_read as usize).min(books.len()); + &books[..n] + } else { + // No progress units and not completed — nothing to apply + return 0; + }; + + let mut newly_applied: u32 = 0; + + for book in books_to_mark { + // Check if already completed using pre-fetched progress — skip if so + if let Some(progress) = progress_map.get(&book.id) + && progress.completed + { + continue; // Already read, skip + } + + // Mark as read (this is a write — must be a real query) + match ReadProgressRepository::mark_as_read(db, user_id, book.id, book.page_count).await { + Ok(_) => { + newly_applied += 1; + } + Err(e) => { + warn!( + "Task {}: Failed to mark book {} as read: {}", + task_id, book.id, e + ); + } + } + } + + newly_applied +} diff --git a/src/tasks/handlers/user_plugin_sync/push.rs b/src/tasks/handlers/user_plugin_sync/push.rs new file mode 100644 index 00000000..dd9c2f68 --- /dev/null +++ b/src/tasks/handlers/user_plugin_sync/push.rs @@ -0,0 +1,528 @@ +//! Push operations — build entries from local reading progress to push to +//! external services. + +use sea_orm::DatabaseConnection; +use std::collections::{HashMap, HashSet}; +use tracing::{debug, warn}; +use uuid::Uuid; + +use crate::db::repositories::{ + BookRepository, ReadProgressRepository, SeriesExternalIdRepository, SeriesMetadataRepository, + UserSeriesRatingRepository, +}; +use crate::services::plugin::sync::{SyncEntry, SyncProgress, SyncReadingStatus}; + +use super::settings::CodexSyncSettings; + +/// Build push entries from a user's Codex reading progress. +/// +/// For each series that has an external ID matching the given source, +/// aggregates book-level reading progress into a single `SyncEntry`. +/// Behaviour is controlled by `CodexSyncSettings` (which series to +/// include, whether partial-progress books count, ratings). +pub(crate) async fn build_push_entries( + db: &DatabaseConnection, + user_id: Uuid, + external_id_source: &str, + task_id: Uuid, + settings: &CodexSyncSettings, +) -> Vec { + // 1. Get all series that have external IDs for this source (1 query) + let external_ids = + match SeriesExternalIdRepository::find_by_source(db, external_id_source).await { + Ok(ids) => ids, + Err(e) => { + warn!( + "Task {}: Failed to fetch external IDs for source {}: {}", + task_id, external_id_source, e + ); + return vec![]; + } + }; + + debug!( + "Task {}: Found {} series with external IDs for source {}", + task_id, + external_ids.len(), + external_id_source + ); + + let external_ids_count = external_ids.len(); + let matched_series_ids: HashSet = external_ids.iter().map(|e| e.series_id).collect(); + + if external_ids.is_empty() && !settings.search_fallback { + return vec![]; + } + + // Collect all series IDs for batch queries + let series_ids: Vec = external_ids.iter().map(|e| e.series_id).collect(); + + // 2. Batch-fetch all books grouped by series (1 query instead of N) + let books_map = match BookRepository::get_by_series_ids(db, &series_ids).await { + Ok(map) => map, + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch books for {} series: {}", + task_id, + series_ids.len(), + e + ); + return vec![]; + } + }; + + // Collect all book IDs for batch progress lookup + let all_book_ids: Vec = books_map.values().flatten().map(|b| b.id).collect(); + + // 3. Batch-fetch all reading progress for these books (1 query instead of N*M) + let progress_map = + match ReadProgressRepository::get_for_user_books(db, user_id, &all_book_ids).await { + Ok(map) => map, + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch reading progress: {}", + task_id, e + ); + HashMap::new() + } + }; + + // 4. Batch-fetch all series metadata (1 query instead of N) + let metadata_map = match SeriesMetadataRepository::get_by_series_ids(db, &series_ids).await { + Ok(map) => map, + Err(e) => { + warn!( + "Task {}: Failed to batch-fetch series metadata: {}", + task_id, e + ); + HashMap::new() + } + }; + + // 5. Batch-fetch all user ratings (1 query — already batched) + let ratings_map: HashMap = + if settings.sync_ratings { + match UserSeriesRatingRepository::get_all_for_user(db, user_id).await { + Ok(ratings) => ratings.into_iter().map(|r| (r.series_id, r)).collect(), + Err(e) => { + warn!( + "Task {}: Failed to fetch user ratings for push: {}", + task_id, e + ); + HashMap::new() + } + } + } else { + HashMap::new() + }; + + // Now iterate using in-memory lookups only — zero additional queries + let mut entries = Vec::new(); + + for ext_id in &external_ids { + let books = match books_map.get(&ext_id.series_id) { + Some(b) if !b.is_empty() => b, + _ => continue, + }; + + // Check reading progress for each book using the pre-fetched map + let mut completed_count: i32 = 0; + let mut in_progress_count: i32 = 0; + let mut has_any_progress = false; + let mut earliest_started: Option> = None; + let mut latest_completed_at: Option> = None; + let mut latest_updated_at: Option> = None; + + for book in books { + if let Some(progress) = progress_map.get(&book.id) { + has_any_progress = true; + if progress.completed { + completed_count += 1; + if let Some(cat) = progress.completed_at { + latest_completed_at = Some(match latest_completed_at { + Some(existing) if cat > existing => cat, + Some(existing) => existing, + None => cat, + }); + } + } else { + in_progress_count += 1; + } + earliest_started = Some(match earliest_started { + Some(existing) if progress.started_at < existing => progress.started_at, + Some(existing) => existing, + None => progress.started_at, + }); + latest_updated_at = Some(match latest_updated_at { + Some(existing) if progress.updated_at > existing => progress.updated_at, + Some(existing) => existing, + None => progress.updated_at, + }); + } + } + + // Skip series with no progress at all + if !has_any_progress { + debug!( + "Task {}: Skipping series {} (ext_id={}) — no reading progress", + task_id, ext_id.series_id, ext_id.external_id + ); + continue; + } + + let all_completed = completed_count == books.len() as i32; + let is_in_progress = !all_completed; + + // Apply Codex sync settings filters + if all_completed && !settings.include_completed { + debug!( + "Task {}: Skipping series {} (ext_id={}) — completed but includeCompleted=false", + task_id, ext_id.series_id, ext_id.external_id + ); + continue; + } + if is_in_progress && !settings.include_in_progress { + debug!( + "Task {}: Skipping series {} (ext_id={}) — in-progress but includeInProgress=false", + task_id, ext_id.series_id, ext_id.external_id + ); + continue; + } + + // Calculate progress count based on settings + let progress_count = if settings.count_partial_progress { + completed_count + in_progress_count + } else { + completed_count + }; + + debug!( + "Task {}: Series {} (ext_id={}): {}/{} books completed, {} in-progress, progress_count={}", + task_id, + ext_id.series_id, + ext_id.external_id, + completed_count, + books.len(), + in_progress_count, + progress_count, + ); + + // Use pre-fetched series metadata (for total_book_count) + let total_book_count = metadata_map + .get(&ext_id.series_id) + .and_then(|m| m.total_book_count) + .filter(|&total| total > 0); + + // Mark as Completed only when: + // 1. All local books are read, AND + // 2. The series has a known total_book_count in metadata, AND + // 3. completed_count >= total_book_count + // Otherwise default to Reading — we can't be sure the library is complete. + let status = if all_completed { + let is_truly_complete = total_book_count.is_some_and(|total| completed_count >= total); + if is_truly_complete { + SyncReadingStatus::Completed + } else { + SyncReadingStatus::Reading + } + } else { + SyncReadingStatus::Reading + }; + + // Server always sends books-read as `volumes`. Codex tracks books + // (each file = 1 volume), not chapters. `chapters` is left `None`. + // The plugin decides how to map this to service-specific fields + // (e.g. AniList's `progress` vs `progressVolumes` based on its own + // `progressUnit` config). + let progress = SyncProgress { + chapters: None, + volumes: Some(progress_count), + pages: None, + total_chapters: None, + total_volumes: total_book_count, + }; + + // Look up rating/notes if sync_ratings is enabled + let (score, notes) = if settings.sync_ratings { + match ratings_map.get(&ext_id.series_id) { + Some(r) => (Some(r.rating as f64), r.notes.clone()), + None => (None, None), + } + } else { + (None, None) + }; + + entries.push(SyncEntry { + external_id: ext_id.external_id.clone(), + status: status.clone(), + progress: Some(progress), + score, + started_at: earliest_started.map(|dt| dt.to_rfc3339()), + completed_at: if status == SyncReadingStatus::Completed { + latest_completed_at.map(|dt| dt.to_rfc3339()) + } else { + None + }, + notes, + latest_updated_at: latest_updated_at.map(|dt| dt.to_rfc3339()), + title: metadata_map.get(&ext_id.series_id).map(|m| m.title.clone()), + }); + } + + let matched_count = entries.len(); + + debug!( + "Task {}: Built {} push entries from {} series with external IDs", + task_id, matched_count, external_ids_count + ); + + // When search_fallback is enabled, also include series that have reading + // progress but no external ID for this source. The plugin will search by title. + if settings.search_fallback { + let unmatched = + build_unmatched_entries(db, user_id, task_id, settings, &matched_series_ids).await; + + debug!( + "Task {}: Built {} unmatched entries for search fallback", + task_id, + unmatched.len() + ); + + entries.extend(unmatched); + } + + entries +} + +/// Build push entries for series that have reading progress but no external ID +/// for the given source. These entries have `external_id: ""` and `title` set, +/// so the plugin can search the external service by title. +async fn build_unmatched_entries( + db: &DatabaseConnection, + user_id: Uuid, + task_id: Uuid, + settings: &CodexSyncSettings, + matched_series_ids: &HashSet, +) -> Vec { + // 1. Get all reading progress for this user + let all_progress = match ReadProgressRepository::get_by_user(db, user_id).await { + Ok(p) => p, + Err(e) => { + warn!( + "Task {}: Failed to fetch user reading progress for search fallback: {}", + task_id, e + ); + return vec![]; + } + }; + + if all_progress.is_empty() { + return vec![]; + } + + // 2. Get book IDs → look up books → get series IDs + let book_ids: Vec = all_progress.iter().map(|p| p.book_id).collect(); + let books = match BookRepository::get_by_ids(db, &book_ids).await { + Ok(b) => b, + Err(e) => { + warn!( + "Task {}: Failed to fetch books for search fallback: {}", + task_id, e + ); + return vec![]; + } + }; + + // Map book_id → series_id + let book_to_series: HashMap = books.iter().map(|b| (b.id, b.series_id)).collect(); + + // Collect unmatched series IDs (have progress but no external ID for this source) + let mut unmatched_series_ids: HashSet = HashSet::new(); + for progress in &all_progress { + if let Some(&series_id) = book_to_series.get(&progress.book_id) + && !matched_series_ids.contains(&series_id) + { + unmatched_series_ids.insert(series_id); + } + } + + if unmatched_series_ids.is_empty() { + return vec![]; + } + + let unmatched_ids_vec: Vec = unmatched_series_ids.iter().copied().collect(); + + // 3. Batch-fetch books, progress, and metadata for unmatched series + let books_map = match BookRepository::get_by_series_ids(db, &unmatched_ids_vec).await { + Ok(m) => m, + Err(e) => { + warn!( + "Task {}: Failed to fetch books for unmatched series: {}", + task_id, e + ); + return vec![]; + } + }; + + let all_book_ids: Vec = books_map.values().flatten().map(|b| b.id).collect(); + let progress_map = + match ReadProgressRepository::get_for_user_books(db, user_id, &all_book_ids).await { + Ok(m) => m, + Err(e) => { + warn!( + "Task {}: Failed to fetch progress for unmatched series: {}", + task_id, e + ); + HashMap::new() + } + }; + + let metadata_map = + match SeriesMetadataRepository::get_by_series_ids(db, &unmatched_ids_vec).await { + Ok(m) => m, + Err(e) => { + warn!( + "Task {}: Failed to fetch metadata for unmatched series: {}", + task_id, e + ); + HashMap::new() + } + }; + + let ratings_map: HashMap = + if settings.sync_ratings { + match UserSeriesRatingRepository::get_all_for_user(db, user_id).await { + Ok(ratings) => ratings.into_iter().map(|r| (r.series_id, r)).collect(), + Err(e) => { + warn!( + "Task {}: Failed to fetch ratings for unmatched series: {}", + task_id, e + ); + HashMap::new() + } + } + } else { + HashMap::new() + }; + + // 4. Build entries — same logic as matched entries, but with external_id: "" + let mut entries = Vec::new(); + + for &series_id in &unmatched_series_ids { + let title = match metadata_map.get(&series_id) { + Some(m) => m.title.clone(), + None => continue, // Skip series without metadata — we need a title for search + }; + + let books = match books_map.get(&series_id) { + Some(b) if !b.is_empty() => b, + _ => continue, + }; + + let mut completed_count: i32 = 0; + let mut in_progress_count: i32 = 0; + let mut has_any_progress = false; + let mut earliest_started: Option> = None; + let mut latest_completed_at: Option> = None; + let mut latest_updated_at: Option> = None; + + for book in books { + if let Some(progress) = progress_map.get(&book.id) { + has_any_progress = true; + if progress.completed { + completed_count += 1; + if let Some(cat) = progress.completed_at { + latest_completed_at = Some(match latest_completed_at { + Some(existing) if cat > existing => cat, + Some(existing) => existing, + None => cat, + }); + } + } else { + in_progress_count += 1; + } + earliest_started = Some(match earliest_started { + Some(existing) if progress.started_at < existing => progress.started_at, + Some(existing) => existing, + None => progress.started_at, + }); + latest_updated_at = Some(match latest_updated_at { + Some(existing) if progress.updated_at > existing => progress.updated_at, + Some(existing) => existing, + None => progress.updated_at, + }); + } + } + + if !has_any_progress { + continue; + } + + let all_completed = completed_count == books.len() as i32; + let is_in_progress = !all_completed; + + if all_completed && !settings.include_completed { + continue; + } + if is_in_progress && !settings.include_in_progress { + continue; + } + + let progress_count = if settings.count_partial_progress { + completed_count + in_progress_count + } else { + completed_count + }; + + let total_book_count = metadata_map + .get(&series_id) + .and_then(|m| m.total_book_count) + .filter(|&total| total > 0); + + let status = if all_completed { + let is_truly_complete = total_book_count.is_some_and(|total| completed_count >= total); + if is_truly_complete { + SyncReadingStatus::Completed + } else { + SyncReadingStatus::Reading + } + } else { + SyncReadingStatus::Reading + }; + + let progress = SyncProgress { + chapters: None, + volumes: Some(progress_count), + pages: None, + total_chapters: None, + total_volumes: total_book_count, + }; + + let (score, notes) = if settings.sync_ratings { + match ratings_map.get(&series_id) { + Some(r) => (Some(r.rating as f64), r.notes.clone()), + None => (None, None), + } + } else { + (None, None) + }; + + entries.push(SyncEntry { + external_id: String::new(), + status: status.clone(), + progress: Some(progress), + score, + started_at: earliest_started.map(|dt| dt.to_rfc3339()), + completed_at: if status == SyncReadingStatus::Completed { + latest_completed_at.map(|dt| dt.to_rfc3339()) + } else { + None + }, + notes, + latest_updated_at: latest_updated_at.map(|dt| dt.to_rfc3339()), + title: Some(title), + }); + } + + entries +} diff --git a/src/tasks/handlers/user_plugin_sync/settings.rs b/src/tasks/handlers/user_plugin_sync/settings.rs new file mode 100644 index 00000000..8a1abc83 --- /dev/null +++ b/src/tasks/handlers/user_plugin_sync/settings.rs @@ -0,0 +1,76 @@ +//! Codex generic sync settings — server-interpreted preferences that control +//! which entries to build and send to the plugin. + +/// JSON key for the Codex-reserved namespace in user plugin config. +/// +/// User plugin config objects may contain a `_codex` key whose value holds +/// server-interpreted preferences (e.g. `includeCompleted`, `syncRatings`). +/// The plugin itself never reads this namespace — it controls server behavior. +pub(crate) const CODEX_CONFIG_NAMESPACE: &str = "_codex"; + +/// Codex generic sync settings — server-interpreted preferences that control +/// which entries to build and send to the plugin. Stored in the user plugin +/// config under the `_codex` namespace (e.g. `config._codex.includeCompleted`). +/// +/// These are NOT plugin config — the plugin never reads them. They control +/// the server's data-source behavior: filtering, progress counting, ratings. +#[derive(Debug, Clone)] +pub(crate) struct CodexSyncSettings { + /// Include series where all local books are marked as read. Default: true. + pub include_completed: bool, + /// Include series where at least one book has been started. Default: true. + pub include_in_progress: bool, + /// Count partially-read books in the progress count. Default: false. + pub count_partial_progress: bool, + /// Include scores and notes in push/pull. Default: true. + pub sync_ratings: bool, + /// Include series without external IDs (for title-based search fallback). + /// When enabled, entries with `external_id: ""` and `title` populated are + /// sent so the plugin can search the external service by title. Default: false. + pub search_fallback: bool, +} + +impl CodexSyncSettings { + /// Parse Codex sync settings from the `_codex` namespace in user plugin config. + /// + /// Example config shape: + /// ```json + /// { + /// "_codex": { + /// "includeCompleted": true, + /// "includeInProgress": true, + /// "countPartialProgress": false, + /// "syncRatings": true + /// }, + /// "progressUnit": "volumes", + /// ... + /// } + /// ``` + pub fn from_user_config(config: &serde_json::Value) -> Self { + let codex = config + .get(CODEX_CONFIG_NAMESPACE) + .unwrap_or(&serde_json::Value::Null); + Self { + include_completed: codex + .get("includeCompleted") + .and_then(|v| v.as_bool()) + .unwrap_or(true), + include_in_progress: codex + .get("includeInProgress") + .and_then(|v| v.as_bool()) + .unwrap_or(true), + count_partial_progress: codex + .get("countPartialProgress") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + sync_ratings: codex + .get("syncRatings") + .and_then(|v| v.as_bool()) + .unwrap_or(true), + search_fallback: codex + .get("searchFallback") + .and_then(|v| v.as_bool()) + .unwrap_or(false), + } + } +} diff --git a/src/tasks/handlers/user_plugin_sync/tests.rs b/src/tasks/handlers/user_plugin_sync/tests.rs new file mode 100644 index 00000000..c15980ae --- /dev/null +++ b/src/tasks/handlers/user_plugin_sync/tests.rs @@ -0,0 +1,1970 @@ +use super::*; +use crate::db::ScanningStrategy; +use crate::db::entities::{books, users}; +use crate::db::repositories::{ + BookRepository, LibraryRepository, ReadProgressRepository, SeriesExternalIdRepository, + SeriesMetadataRepository, SeriesRepository, UserRepository, UserSeriesRatingRepository, +}; +use crate::db::test_helpers::create_test_db; +use crate::services::plugin::sync::{SyncEntry, SyncProgress, SyncReadingStatus}; +use chrono::Utc; + +/// Helper to create a test user in the database +async fn create_test_user(db: &sea_orm::DatabaseConnection) -> users::Model { + let user = users::Model { + id: Uuid::new_v4(), + username: format!("syncuser_{}", Uuid::new_v4()), + email: format!("sync_{}@example.com", Uuid::new_v4()), + password_hash: "hash123".to_string(), + role: "reader".to_string(), + is_active: true, + email_verified: false, + permissions: serde_json::json!([]), + created_at: Utc::now(), + updated_at: Utc::now(), + last_login_at: None, + }; + UserRepository::create(db, &user).await.unwrap() +} + +/// Helper to create a book in a series with a given page count +async fn create_test_book( + db: &sea_orm::DatabaseConnection, + series_id: Uuid, + library_id: Uuid, + index: usize, + page_count: i32, +) -> books::Model { + let book = books::Model { + id: Uuid::new_v4(), + series_id, + library_id, + file_path: format!("/test/book_{}_{}.cbz", index, Uuid::new_v4()), + file_name: format!("book_{}.cbz", index), + file_size: 1024, + file_hash: format!("hash_{}_{}", index, Uuid::new_v4()), + partial_hash: String::new(), + format: "cbz".to_string(), + page_count, + deleted: false, + analyzed: false, + 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, + }; + BookRepository::create(db, &book, None).await.unwrap() +} + +#[test] +fn test_handler_creation() { + // Handler requires a PluginManager, verify the struct is constructed correctly + // (actual integration test would need a real PluginManager) +} + +#[test] +fn test_sync_result_serialization() { + let result = UserPluginSyncResult { + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + external_username: Some("manga_reader".to_string()), + pushed: 5, + pulled: 10, + matched: 8, + applied: 6, + push_failures: 1, + pull_incomplete: false, + pull_error: None, + push_error: None, + skipped_reason: None, + }; + + let json = serde_json::to_value(&result).unwrap(); + assert_eq!(json["externalUsername"], "manga_reader"); + assert_eq!(json["pushed"], 5); + assert_eq!(json["pulled"], 10); + assert_eq!(json["matched"], 8); + assert_eq!(json["applied"], 6); + assert_eq!(json["pushFailures"], 1); + assert!(!json["pullIncomplete"].as_bool().unwrap()); + assert!(!json.as_object().unwrap().contains_key("skippedReason")); + assert!(!json.as_object().unwrap().contains_key("pullError")); + assert!(!json.as_object().unwrap().contains_key("pushError")); +} + +#[test] +fn test_sync_result_with_errors() { + let result = UserPluginSyncResult { + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + external_username: Some("user".to_string()), + pushed: 3, + pulled: 0, + matched: 0, + applied: 0, + push_failures: 0, + pull_incomplete: false, + pull_error: Some("AniList API error: 400 Bad Request".to_string()), + push_error: None, + skipped_reason: None, + }; + + let json = serde_json::to_value(&result).unwrap(); + assert_eq!(json["pullError"], "AniList API error: 400 Bad Request"); + assert!(!json.as_object().unwrap().contains_key("pushError")); + assert_eq!(json["pushed"], 3); + assert_eq!(json["pulled"], 0); + + // Round-trip + let deserialized: UserPluginSyncResult = serde_json::from_value(json).unwrap(); + assert_eq!( + deserialized.pull_error, + Some("AniList API error: 400 Bad Request".to_string()) + ); + assert!(deserialized.push_error.is_none()); +} + +#[test] +fn test_sync_result_with_both_errors() { + let result = UserPluginSyncResult { + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + external_username: None, + pushed: 0, + pulled: 0, + matched: 0, + applied: 0, + push_failures: 0, + pull_incomplete: false, + pull_error: Some("Pull failed".to_string()), + push_error: Some("Push failed".to_string()), + skipped_reason: None, + }; + + let json = serde_json::to_value(&result).unwrap(); + assert_eq!(json["pullError"], "Pull failed"); + assert_eq!(json["pushError"], "Push failed"); +} + +#[test] +fn test_sync_result_skipped() { + let result = UserPluginSyncResult { + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + external_username: None, + pushed: 0, + pulled: 0, + matched: 0, + applied: 0, + push_failures: 0, + pull_incomplete: false, + pull_error: None, + push_error: None, + skipped_reason: Some("plugin_not_enabled".to_string()), + }; + + let json = serde_json::to_value(&result).unwrap(); + assert_eq!(json["skippedReason"], "plugin_not_enabled"); + assert!(!json.as_object().unwrap().contains_key("externalUsername")); + assert_eq!(json["pushed"], 0); + assert_eq!(json["pulled"], 0); + assert_eq!(json["matched"], 0); + assert_eq!(json["applied"], 0); +} + +#[test] +fn test_sync_result_deserialization() { + let json = serde_json::json!({ + "pluginId": "00000000-0000-0000-0000-000000000001", + "userId": "00000000-0000-0000-0000-000000000002", + "externalUsername": "test_user", + "pushed": 3, + "pulled": 7, + "matched": 5, + "applied": 4, + "pushFailures": 0, + "pullIncomplete": true, + }); + + let result: UserPluginSyncResult = serde_json::from_value(json).unwrap(); + assert_eq!(result.external_username, Some("test_user".to_string())); + assert_eq!(result.pushed, 3); + assert_eq!(result.pulled, 7); + assert_eq!(result.matched, 5); + assert_eq!(result.applied, 4); + assert!(result.pull_incomplete); + assert!(result.skipped_reason.is_none()); +} + +#[test] +fn test_sync_result_pull_incomplete() { + let result = UserPluginSyncResult { + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + external_username: Some("user".to_string()), + pushed: 0, + pulled: 500, + matched: 300, + applied: 250, + push_failures: 0, + pull_incomplete: true, + pull_error: None, + push_error: None, + skipped_reason: None, + }; + + let json = serde_json::to_value(&result).unwrap(); + assert!(json["pullIncomplete"].as_bool().unwrap()); + assert_eq!(json["pulled"], 500); + assert_eq!(json["matched"], 300); + assert_eq!(json["applied"], 250); +} + +#[test] +fn test_sync_result_applied_field() { + let result = UserPluginSyncResult { + plugin_id: Uuid::new_v4(), + user_id: Uuid::new_v4(), + external_username: None, + pushed: 0, + pulled: 10, + matched: 5, + applied: 3, + push_failures: 0, + pull_incomplete: false, + pull_error: None, + push_error: None, + skipped_reason: None, + }; + + let json = serde_json::to_value(&result).unwrap(); + assert_eq!(json["applied"], 3); + + // Verify round-trip + let deserialized: UserPluginSyncResult = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized.applied, 3); +} + +#[tokio::test] +async fn test_match_and_apply_no_source() { + let (db, _temp_dir) = create_test_db().await; + let user_id = Uuid::new_v4(); + + let entries = vec![SyncEntry { + external_id: "12345".to_string(), + status: SyncReadingStatus::Reading, + progress: None, + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }]; + + let (matched, applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + None, + user_id, + Uuid::new_v4(), + false, + ) + .await; + assert_eq!(matched, 0); + assert_eq!(applied, 0); +} + +#[tokio::test] +async fn test_match_and_apply_with_matches() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "My Manga", None) + .await + .unwrap(); + + // Create an api:anilist external ID for the series + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "12345", + None, + None, + ) + .await + .unwrap(); + + let user_id = Uuid::new_v4(); + + let entries = vec![ + SyncEntry { + external_id: "12345".to_string(), // matches + status: SyncReadingStatus::Reading, + progress: None, + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }, + SyncEntry { + external_id: "99999".to_string(), // no match + status: SyncReadingStatus::Completed, + progress: None, + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }, + ]; + + let (matched, _applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + Some("api:anilist"), + user_id, + Uuid::new_v4(), + false, + ) + .await; + assert_eq!(matched, 1); +} + +#[tokio::test] +async fn test_match_and_apply_pulled_entries_applies_progress() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Test Manga", None) + .await + .unwrap(); + + // Create 5 books in the series + for i in 1..=5 { + create_test_book(db.sea_orm_connection(), series.id, library.id, i, 100).await; + } + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "42", + None, + None, + ) + .await + .unwrap(); + + let user = create_test_user(db.sea_orm_connection()).await; + let user_id = user.id; + + // Pull entry says 3 chapters read + let entries = vec![SyncEntry { + external_id: "42".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: Some(3), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }]; + + let (matched, applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + Some("api:anilist"), + user_id, + Uuid::new_v4(), + false, + ) + .await; + assert_eq!(matched, 1); + assert_eq!(applied, 3); + + // Verify: first 3 books should be marked as read + let books_list = BookRepository::list_by_series(db.sea_orm_connection(), series.id, false) + .await + .unwrap(); + for (i, book) in books_list.iter().enumerate() { + let progress = + ReadProgressRepository::get_by_user_and_book(db.sea_orm_connection(), user_id, book.id) + .await + .unwrap(); + if i < 3 { + assert!(progress.is_some(), "Book {} should have progress", i); + assert!( + progress.unwrap().completed, + "Book {} should be completed", + i + ); + } else { + assert!(progress.is_none(), "Book {} should have no progress", i); + } + } +} + +#[tokio::test] +async fn test_match_and_apply_skips_already_read() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Test Manga", None) + .await + .unwrap(); + + // Create 3 books + let mut book_ids = Vec::new(); + for i in 1..=3 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 50).await; + book_ids.push(book.id); + } + + let user = create_test_user(db.sea_orm_connection()).await; + let user_id = user.id; + + // Pre-mark book 1 as read + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user_id, book_ids[0], 50) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "99", + None, + None, + ) + .await + .unwrap(); + + // Pull says completed (all 3 chapters) + let entries = vec![SyncEntry { + external_id: "99".to_string(), + status: SyncReadingStatus::Completed, + progress: Some(SyncProgress { + chapters: Some(3), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }]; + + let (matched, applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + Some("api:anilist"), + user_id, + Uuid::new_v4(), + false, + ) + .await; + assert_eq!(matched, 1); + // Only 2 books newly applied (book 1 was already read) + assert_eq!(applied, 2); +} + +/// Default Codex sync settings for tests (matches production defaults) +fn default_codex_settings() -> CodexSyncSettings { + CodexSyncSettings { + include_completed: true, + include_in_progress: true, + count_partial_progress: false, + sync_ratings: true, + search_fallback: false, + } +} + +#[tokio::test] +async fn test_build_push_entries_with_progress() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Push Manga", None) + .await + .unwrap(); + + // Create 4 books + let mut test_books = Vec::new(); + for i in 1..=4 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 100).await; + test_books.push(book); + } + + let user = create_test_user(db.sea_orm_connection()).await; + let user_id = user.id; + + // Mark first 2 books as read + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user_id, test_books[0].id, 100) + .await + .unwrap(); + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user_id, test_books[1].id, 100) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "777", + None, + None, + ) + .await + .unwrap(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user_id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].external_id, "777"); + assert_eq!(entries[0].status, SyncReadingStatus::Reading); + // "volumes" mode sends only volumes (not chapters, to avoid misleading activity) + assert_eq!(entries[0].progress.as_ref().unwrap().volumes, Some(2)); + assert!(entries[0].progress.as_ref().unwrap().chapters.is_none()); +} + +#[tokio::test] +async fn test_build_push_entries_all_completed() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Done Manga", None) + .await + .unwrap(); + + // Create 2 books + let mut test_books = Vec::new(); + for i in 1..=2 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 50).await; + test_books.push(book); + } + + let user = create_test_user(db.sea_orm_connection()).await; + let user_id = user.id; + + // Mark all books as read + for book in &test_books { + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user_id, book.id, 50) + .await + .unwrap(); + } + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "888", + None, + None, + ) + .await + .unwrap(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user_id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + assert_eq!(entries.len(), 1); + // Always push as Reading — we can't know total chapter count from external service + assert_eq!(entries[0].status, SyncReadingStatus::Reading); + // "volumes" mode sends only volumes (not chapters, to avoid misleading activity) + assert_eq!(entries[0].progress.as_ref().unwrap().volumes, Some(2)); + assert!(entries[0].progress.as_ref().unwrap().chapters.is_none()); + assert!(entries[0].completed_at.is_none()); +} + +#[tokio::test] +async fn test_build_push_entries_skips_no_progress() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Unread Manga", None) + .await + .unwrap(); + + // Create a book with no progress + create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "999", + None, + None, + ) + .await + .unwrap(); + + let user_id = Uuid::new_v4(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user_id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + // No progress → should skip + assert!(entries.is_empty()); +} + +#[test] +fn test_sync_mode_parsing_default_is_both() { + // When config has no syncMode key, default to "both" + let config = serde_json::json!({}); + let sync_mode = config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both"); + assert_eq!(sync_mode, "both"); + let do_pull = sync_mode == "both" || sync_mode == "pull"; + let do_push = sync_mode == "both" || sync_mode == "push"; + assert!(do_pull); + assert!(do_push); +} + +#[test] +fn test_sync_mode_parsing_pull_only() { + let config = serde_json::json!({"syncMode": "pull"}); + let sync_mode = config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both"); + assert_eq!(sync_mode, "pull"); + let do_pull = sync_mode == "both" || sync_mode == "pull"; + let do_push = sync_mode == "both" || sync_mode == "push"; + assert!(do_pull); + assert!(!do_push); +} + +#[test] +fn test_sync_mode_parsing_push_only() { + let config = serde_json::json!({"syncMode": "push"}); + let sync_mode = config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both"); + assert_eq!(sync_mode, "push"); + let do_pull = sync_mode == "both" || sync_mode == "pull"; + let do_push = sync_mode == "both" || sync_mode == "push"; + assert!(!do_pull); + assert!(do_push); +} + +#[test] +fn test_sync_mode_parsing_both_explicit() { + let config = serde_json::json!({"syncMode": "both"}); + let sync_mode = config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both"); + assert_eq!(sync_mode, "both"); + let do_pull = sync_mode == "both" || sync_mode == "pull"; + let do_push = sync_mode == "both" || sync_mode == "push"; + assert!(do_pull); + assert!(do_push); +} + +#[test] +fn test_sync_mode_parsing_invalid_value_disables_both() { + // An unrecognized syncMode value should disable both pull and push + let config = serde_json::json!({"syncMode": "invalid"}); + let sync_mode = config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both"); + assert_eq!(sync_mode, "invalid"); + let do_pull = sync_mode == "both" || sync_mode == "pull"; + let do_push = sync_mode == "both" || sync_mode == "push"; + assert!(!do_pull); + assert!(!do_push); +} + +#[test] +fn test_sync_mode_parsing_non_string_falls_back_to_both() { + // If syncMode is a non-string value, as_str() returns None → default "both" + let config = serde_json::json!({"syncMode": 123}); + let sync_mode = config + .get("syncMode") + .and_then(|v| v.as_str()) + .unwrap_or("both"); + assert_eq!(sync_mode, "both"); +} + +#[tokio::test] +async fn test_match_and_apply_empty() { + let (db, _temp_dir) = create_test_db().await; + + let (matched, applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &[], + Some("api:anilist"), + Uuid::new_v4(), + Uuid::new_v4(), + false, + ) + .await; + assert_eq!(matched, 0); + assert_eq!(applied, 0); +} + +#[test] +fn test_codex_settings_defaults() { + let config = serde_json::json!({}); + let settings = CodexSyncSettings::from_user_config(&config); + assert!(settings.include_completed); + assert!(settings.include_in_progress); + assert!(!settings.count_partial_progress); + assert!(settings.sync_ratings); // default is now true +} + +#[test] +fn test_codex_settings_from_user_config() { + let config = serde_json::json!({ + "_codex": { + "includeCompleted": false, + "includeInProgress": true, + "countPartialProgress": true, + "syncRatings": false, + } + }); + let settings = CodexSyncSettings::from_user_config(&config); + assert!(!settings.include_completed); + assert!(settings.include_in_progress); + assert!(settings.count_partial_progress); + assert!(!settings.sync_ratings); +} + +#[tokio::test] +async fn test_build_push_entries_skip_completed_series() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Done Manga 2", None) + .await + .unwrap(); + + // Create 2 books, mark both as read (= completed) + let mut test_books = Vec::new(); + for i in 1..=2 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 50).await; + test_books.push(book); + } + + let user = create_test_user(db.sea_orm_connection()).await; + for book in &test_books { + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 50) + .await + .unwrap(); + } + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "222", + None, + None, + ) + .await + .unwrap(); + + // Disable including completed series + let settings = CodexSyncSettings { + include_completed: false, + ..default_codex_settings() + }; + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + + assert!(entries.is_empty(), "Completed series should be skipped"); +} + +#[tokio::test] +async fn test_build_push_entries_skip_in_progress_series() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "WIP Manga", None) + .await + .unwrap(); + + // Create 3 books, mark only 1 as read (= in-progress) + let mut test_books = Vec::new(); + for i in 1..=3 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 50).await; + test_books.push(book); + } + + let user = create_test_user(db.sea_orm_connection()).await; + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, test_books[0].id, 50) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "333", + None, + None, + ) + .await + .unwrap(); + + // Disable including in-progress series + let settings = CodexSyncSettings { + include_in_progress: false, + ..default_codex_settings() + }; + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + + assert!(entries.is_empty(), "In-progress series should be skipped"); +} + +#[tokio::test] +async fn test_build_push_entries_count_in_progress_volumes() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "IP Manga", None) + .await + .unwrap(); + + // Create 4 books + let mut test_books = Vec::new(); + for i in 1..=4 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 100).await; + test_books.push(book); + } + + let user = create_test_user(db.sea_orm_connection()).await; + + // Mark book 1 as fully read + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, test_books[0].id, 100) + .await + .unwrap(); + + // Mark book 2 as partially read (in-progress) + ReadProgressRepository::upsert( + db.sea_orm_connection(), + user.id, + test_books[1].id, + 50, // current_page + false, // not completed + ) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "444", + None, + None, + ) + .await + .unwrap(); + + // Without partial progress: should count only completed (1) + let settings = default_codex_settings(); + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + assert_eq!(entries.len(), 1); + // Server always sends volumes (not chapters) + assert_eq!(entries[0].progress.as_ref().unwrap().volumes, Some(1)); + assert!(entries[0].progress.as_ref().unwrap().chapters.is_none()); + + // With partial progress: should count completed + in-progress (2) + let settings_with_partial = CodexSyncSettings { + count_partial_progress: true, + ..default_codex_settings() + }; + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings_with_partial, + ) + .await; + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].progress.as_ref().unwrap().volumes, Some(2)); + assert!(entries[0].progress.as_ref().unwrap().chapters.is_none()); +} + +#[tokio::test] +async fn test_apply_pulled_entry_uses_volumes() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Vol Manga", None) + .await + .unwrap(); + + // Create 5 books + for i in 1..=5 { + create_test_book(db.sea_orm_connection(), series.id, library.id, i, 100).await; + } + + let user = create_test_user(db.sea_orm_connection()).await; + + // Pull entry with volumes=2 (no chapters) + let entry = SyncEntry { + external_id: "55".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: None, + volumes: Some(2), + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: None, + started_at: None, + completed_at: None, + notes: None, + latest_updated_at: None, + title: None, + }; + + // Build pre-fetched maps for apply_pulled_entry (via match_and_apply which calls it) + // We test via match_and_apply since apply_pulled_entry is private to pull module + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "55", + None, + None, + ) + .await + .unwrap(); + + let (matched, applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &[entry], + Some("api:anilist"), + user.id, + Uuid::new_v4(), + false, + ) + .await; + assert_eq!(matched, 1); + assert_eq!(applied, 2); + + // Verify first 2 books are marked as read + let books = BookRepository::list_by_series(db.sea_orm_connection(), series.id, false) + .await + .unwrap(); + for (i, book) in books.iter().enumerate() { + let progress = + ReadProgressRepository::get_by_user_and_book(db.sea_orm_connection(), user.id, book.id) + .await + .unwrap(); + if i < 2 { + assert!(progress.is_some(), "Book {} should have progress", i); + assert!( + progress.unwrap().completed, + "Book {} should be completed", + i + ); + } else { + assert!(progress.is_none(), "Book {} should have no progress", i); + } + } +} + +// ========================================================================= +// Rating sync tests +// ========================================================================= + +#[test] +fn test_codex_settings_sync_ratings_default() { + let config = serde_json::json!({}); + let settings = CodexSyncSettings::from_user_config(&config); + assert!(settings.sync_ratings); // default is now true +} + +#[test] +fn test_codex_settings_sync_ratings_disabled() { + let config = serde_json::json!({"_codex": {"syncRatings": false}}); + let settings = CodexSyncSettings::from_user_config(&config); + assert!(!settings.sync_ratings); +} + +#[tokio::test] +async fn test_build_push_entries_includes_rating() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Rated Manga", None) + .await + .unwrap(); + + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 100) + .await + .unwrap(); + + // Create a rating for this series + UserSeriesRatingRepository::create( + db.sea_orm_connection(), + user.id, + series.id, + 85, + Some("Excellent manga!".to_string()), + ) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "555", + None, + None, + ) + .await + .unwrap(); + + let settings = default_codex_settings(); // sync_ratings=true by default + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].score, Some(85.0)); + assert_eq!(entries[0].notes, Some("Excellent manga!".to_string())); +} + +#[tokio::test] +async fn test_build_push_entries_no_rating_when_disabled() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Rated Manga 2", None) + .await + .unwrap(); + + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 100) + .await + .unwrap(); + + // Create a rating, but sync_ratings is false + UserSeriesRatingRepository::create(db.sea_orm_connection(), user.id, series.id, 85, None) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "556", + None, + None, + ) + .await + .unwrap(); + + let settings = CodexSyncSettings { + sync_ratings: false, + ..default_codex_settings() + }; + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + + assert_eq!(entries.len(), 1); + assert!(entries[0].score.is_none()); + assert!(entries[0].notes.is_none()); +} + +#[tokio::test] +async fn test_build_push_entries_no_rating_for_unrated() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Unrated Manga", None) + .await + .unwrap(); + + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 100) + .await + .unwrap(); + + // No rating created + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "557", + None, + None, + ) + .await + .unwrap(); + + let settings = default_codex_settings(); // sync_ratings=true by default + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + + assert_eq!(entries.len(), 1); + assert!(entries[0].score.is_none()); + assert!(entries[0].notes.is_none()); +} + +#[tokio::test] +async fn test_apply_pulled_rating_no_existing() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Pull Manga", None) + .await + .unwrap(); + + create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "600", + None, + None, + ) + .await + .unwrap(); + + let entries = vec![SyncEntry { + external_id: "600".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: Some(1), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: Some(75.0), + started_at: None, + completed_at: None, + notes: Some("Good so far".to_string()), + latest_updated_at: None, + title: None, + }]; + + let (matched, _applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + Some("api:anilist"), + user.id, + Uuid::new_v4(), + true, // sync_ratings=true + ) + .await; + + assert_eq!(matched, 1); + + // Verify rating was created + let rating = UserSeriesRatingRepository::get_by_user_and_series( + db.sea_orm_connection(), + user.id, + series.id, + ) + .await + .unwrap(); + assert!(rating.is_some()); + let rating = rating.unwrap(); + assert_eq!(rating.rating, 75); + assert_eq!(rating.notes, Some("Good so far".to_string())); +} + +#[tokio::test] +async fn test_apply_pulled_rating_existing_not_overwritten() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Rated Manga 3", None) + .await + .unwrap(); + + create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + // Pre-create a Codex rating + UserSeriesRatingRepository::create( + db.sea_orm_connection(), + user.id, + series.id, + 90, + Some("My notes".to_string()), + ) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "601", + None, + None, + ) + .await + .unwrap(); + + // Pull entry with different score + let entries = vec![SyncEntry { + external_id: "601".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: Some(1), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: Some(60.0), + started_at: None, + completed_at: None, + notes: Some("AniList notes".to_string()), + latest_updated_at: None, + title: None, + }]; + + let (_matched, _applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + Some("api:anilist"), + user.id, + Uuid::new_v4(), + true, + ) + .await; + + // Verify Codex rating was NOT overwritten + let rating = UserSeriesRatingRepository::get_by_user_and_series( + db.sea_orm_connection(), + user.id, + series.id, + ) + .await + .unwrap() + .unwrap(); + assert_eq!(rating.rating, 90); // Original Codex rating preserved + assert_eq!(rating.notes, Some("My notes".to_string())); +} + +#[tokio::test] +async fn test_apply_pulled_rating_disabled() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "No Sync Manga", None) + .await + .unwrap(); + + create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "602", + None, + None, + ) + .await + .unwrap(); + + let entries = vec![SyncEntry { + external_id: "602".to_string(), + status: SyncReadingStatus::Reading, + progress: Some(SyncProgress { + chapters: Some(1), + volumes: None, + pages: None, + total_chapters: None, + total_volumes: None, + }), + score: Some(80.0), + started_at: None, + completed_at: None, + notes: Some("Should not be stored".to_string()), + latest_updated_at: None, + title: None, + }]; + + let (_matched, _applied) = pull::match_and_apply_pulled_entries( + db.sea_orm_connection(), + &entries, + Some("api:anilist"), + user.id, + Uuid::new_v4(), + false, // sync_ratings=false + ) + .await; + + // Verify no rating was created + let rating = UserSeriesRatingRepository::get_by_user_and_series( + db.sea_orm_connection(), + user.id, + series.id, + ) + .await + .unwrap(); + assert!(rating.is_none()); +} + +// ========================================================================= +// New tests: latestUpdatedAt, totalVolumes, always-sends-volumes +// ========================================================================= + +#[tokio::test] +async fn test_build_push_entries_populates_latest_updated_at() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Updated Manga", None) + .await + .unwrap(); + + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 100) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "800", + None, + None, + ) + .await + .unwrap(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + assert_eq!(entries.len(), 1); + assert!( + entries[0].latest_updated_at.is_some(), + "latestUpdatedAt should be populated when there is reading progress" + ); +} + +#[tokio::test] +async fn test_build_push_entries_populates_total_volumes() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Total Manga", None) + .await + .unwrap(); + + // Create 2 books + let mut test_books = Vec::new(); + for i in 1..=2 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 100).await; + test_books.push(book); + } + + // Set total_book_count=3 in metadata (more than the 2 local books) + SeriesMetadataRepository::update_total_book_count(db.sea_orm_connection(), series.id, Some(3)) + .await + .unwrap(); + + let user = create_test_user(db.sea_orm_connection()).await; + + // Mark 1 book as read + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, test_books[0].id, 100) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "801", + None, + None, + ) + .await + .unwrap(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + assert_eq!(entries.len(), 1); + assert_eq!( + entries[0].progress.as_ref().unwrap().total_volumes, + Some(3), + "totalVolumes should come from series metadata total_book_count" + ); +} + +#[tokio::test] +async fn test_build_push_entries_always_sends_volumes() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Volumes Manga", None) + .await + .unwrap(); + + let mut test_books = Vec::new(); + for i in 1..=3 { + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, i, 100).await; + test_books.push(book); + } + + let user = create_test_user(db.sea_orm_connection()).await; + + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, test_books[0].id, 100) + .await + .unwrap(); + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, test_books[1].id, 100) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "802", + None, + None, + ) + .await + .unwrap(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + assert_eq!(entries.len(), 1); + let progress = entries[0].progress.as_ref().unwrap(); + assert_eq!( + progress.volumes, + Some(2), + "Server should always send books-read as volumes" + ); + assert!( + progress.chapters.is_none(), + "chapters should be None — server always sends volumes" + ); +} + +// ========================================================================= +// search_fallback settings and unmatched entries tests +// ========================================================================= + +#[test] +fn test_codex_settings_search_fallback_default() { + let config = serde_json::json!({}); + let settings = CodexSyncSettings::from_user_config(&config); + assert!( + !settings.search_fallback, + "search_fallback should default to false" + ); +} + +#[test] +fn test_codex_settings_search_fallback_enabled() { + let config = serde_json::json!({"_codex": {"searchFallback": true}}); + let settings = CodexSyncSettings::from_user_config(&config); + assert!(settings.search_fallback); +} + +#[test] +fn test_codex_settings_search_fallback_disabled() { + let config = serde_json::json!({"_codex": {"searchFallback": false}}); + let settings = CodexSyncSettings::from_user_config(&config); + assert!(!settings.search_fallback); +} + +#[tokio::test] +async fn test_build_push_entries_includes_unmatched_with_search_fallback() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + // Series A: has external ID + let series_a = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Matched Manga", None) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series_a.id, + "api:anilist", + "100", + None, + None, + ) + .await + .unwrap(); + + let book_a = create_test_book(db.sea_orm_connection(), series_a.id, library.id, 1, 50).await; + + // Series B: NO external ID, but has metadata title and reading progress + let series_b = + SeriesRepository::create(db.sea_orm_connection(), library.id, "Unmatched Manga", None) + .await + .unwrap(); + + let book_b = create_test_book(db.sea_orm_connection(), series_b.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + + // Mark books as read in both series + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book_a.id, 50) + .await + .unwrap(); + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book_b.id, 100) + .await + .unwrap(); + + // With search_fallback=false: only matched series + let settings_no_fallback = default_codex_settings(); + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings_no_fallback, + ) + .await; + assert_eq!( + entries.len(), + 1, + "Only matched series without search_fallback" + ); + assert_eq!(entries[0].external_id, "100"); + + // With search_fallback=true: matched + unmatched series + let settings_with_fallback = CodexSyncSettings { + search_fallback: true, + ..default_codex_settings() + }; + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings_with_fallback, + ) + .await; + assert_eq!( + entries.len(), + 2, + "Both matched and unmatched with search_fallback" + ); + + // Check the unmatched entry + let unmatched = entries.iter().find(|e| e.external_id.is_empty()); + assert!( + unmatched.is_some(), + "Unmatched entry should have empty external_id" + ); + let unmatched = unmatched.unwrap(); + assert_eq!( + unmatched.title, + Some("Unmatched Manga".to_string()), + "Unmatched entry should have title from metadata" + ); + assert!(unmatched.progress.is_some()); + assert_eq!(unmatched.progress.as_ref().unwrap().volumes, Some(1)); +} + +#[tokio::test] +async fn test_build_push_entries_unmatched_skips_no_metadata() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + // Series with NO external ID and reading progress but also no series metadata title + // Note: SeriesRepository::create auto-creates metadata with the series name, + // so this series will have metadata. We test that it shows up. + let series = SeriesRepository::create(db.sea_orm_connection(), library.id, "Has Title", None) + .await + .unwrap(); + + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 50).await; + + let user = create_test_user(db.sea_orm_connection()).await; + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 50) + .await + .unwrap(); + + let settings = CodexSyncSettings { + search_fallback: true, + ..default_codex_settings() + }; + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &settings, + ) + .await; + + // Should include the unmatched entry since it has metadata (from series creation) + assert_eq!(entries.len(), 1); + assert!(entries[0].external_id.is_empty()); + assert_eq!(entries[0].title, Some("Has Title".to_string())); +} + +#[tokio::test] +async fn test_build_push_entries_populates_title_for_matched() { + let (db, _temp_dir) = create_test_db().await; + + let library = LibraryRepository::create( + db.sea_orm_connection(), + "Test Library", + "/test/path", + ScanningStrategy::Default, + ) + .await + .unwrap(); + + let series = SeriesRepository::create( + db.sea_orm_connection(), + library.id, + "Title Test Manga", + None, + ) + .await + .unwrap(); + + let book = create_test_book(db.sea_orm_connection(), series.id, library.id, 1, 100).await; + + let user = create_test_user(db.sea_orm_connection()).await; + ReadProgressRepository::mark_as_read(db.sea_orm_connection(), user.id, book.id, 100) + .await + .unwrap(); + + SeriesExternalIdRepository::create( + db.sea_orm_connection(), + series.id, + "api:anilist", + "900", + None, + None, + ) + .await + .unwrap(); + + let entries = push::build_push_entries( + db.sea_orm_connection(), + user.id, + "api:anilist", + Uuid::new_v4(), + &default_codex_settings(), + ) + .await; + + assert_eq!(entries.len(), 1); + assert_eq!( + entries[0].title, + Some("Title Test Manga".to_string()), + "Matched entries should also have title populated from metadata" + ); +} diff --git a/src/tasks/types.rs b/src/tasks/types.rs index 8689205f..6a678b62 100644 --- a/src/tasks/types.rs +++ b/src/tasks/types.rs @@ -141,6 +141,25 @@ pub enum TaskType { #[serde(rename = "seriesIds", default)] series_ids: Option>, // If set, process only these specific series (bulk selection) }, + + /// Clean up expired plugin storage data across all user plugins + CleanupPluginData, + + /// Sync user plugin data with external service + UserPluginSync { + #[serde(rename = "pluginId")] + plugin_id: Uuid, + #[serde(rename = "userId")] + user_id: Uuid, + }, + + /// Refresh recommendations from a user plugin + UserPluginRecommendations { + #[serde(rename = "pluginId")] + plugin_id: Uuid, + #[serde(rename = "userId")] + user_id: Uuid, + }, } fn default_mode() -> String { @@ -168,6 +187,9 @@ impl TaskType { TaskType::PluginAutoMatch { .. } => "plugin_auto_match", TaskType::ReprocessSeriesTitle { .. } => "reprocess_series_title", TaskType::ReprocessSeriesTitles { .. } => "reprocess_series_titles", + TaskType::CleanupPluginData => "cleanup_plugin_data", + TaskType::UserPluginSync { .. } => "user_plugin_sync", + TaskType::UserPluginRecommendations { .. } => "user_plugin_recommendations", } } @@ -239,6 +261,12 @@ impl TaskType { TaskType::ReprocessSeriesTitles { series_ids, .. } => { serde_json::json!({ "series_ids": series_ids }) } + TaskType::UserPluginSync { plugin_id, user_id } => { + serde_json::json!({ "plugin_id": plugin_id, "user_id": user_id }) + } + TaskType::UserPluginRecommendations { plugin_id, user_id } => { + serde_json::json!({ "plugin_id": plugin_id, "user_id": user_id }) + } _ => serde_json::json!({}), } } diff --git a/src/tasks/worker.rs b/src/tasks/worker.rs index 44b0a701..e6166bbb 100644 --- a/src/tasks/worker.rs +++ b/src/tasks/worker.rs @@ -21,14 +21,16 @@ use crate::db::repositories::TaskRepository; use crate::events::{EventBroadcaster, RecordedEvent, TaskProgressEvent}; use crate::services::PdfPageCache; use crate::services::plugin::PluginManager; +use crate::services::user_plugin::OAuthStateManager; use crate::services::{SettingsService, TaskMetricsService, ThumbnailService}; use crate::tasks::error::check_rate_limited; use crate::tasks::handlers::{ AnalyzeBookHandler, AnalyzeSeriesHandler, CleanupBookFilesHandler, CleanupOrphanedFilesHandler, - CleanupPdfCacheHandler, CleanupSeriesFilesHandler, FindDuplicatesHandler, - GenerateSeriesThumbnailHandler, GenerateSeriesThumbnailsHandler, GenerateThumbnailHandler, - GenerateThumbnailsHandler, PluginAutoMatchHandler, PurgeDeletedHandler, - ReprocessSeriesTitleHandler, ReprocessSeriesTitlesHandler, ScanLibraryHandler, TaskHandler, + CleanupPdfCacheHandler, CleanupPluginDataHandler, CleanupSeriesFilesHandler, + FindDuplicatesHandler, GenerateSeriesThumbnailHandler, GenerateSeriesThumbnailsHandler, + GenerateThumbnailHandler, GenerateThumbnailsHandler, PluginAutoMatchHandler, + PurgeDeletedHandler, ReprocessSeriesTitleHandler, ReprocessSeriesTitlesHandler, + ScanLibraryHandler, TaskHandler, UserPluginRecommendationsHandler, UserPluginSyncHandler, }; /// Task worker that processes tasks from the queue @@ -81,6 +83,11 @@ impl TaskWorker { "reprocess_series_titles".to_string(), Arc::new(ReprocessSeriesTitlesHandler::new()), ); + // Plugin data cleanup handler (no dependencies) + handlers.insert( + "cleanup_plugin_data".to_string(), + Arc::new(CleanupPluginDataHandler::new()), + ); // Generate worker ID from hostname or random UUID let worker_id = std::env::var("HOSTNAME") @@ -119,6 +126,18 @@ impl TaskWorker { self } + /// Set the OAuth state manager for cleaning up expired OAuth flows + /// + /// This re-registers the `CleanupPluginDataHandler` with the manager so it + /// can clean up expired in-memory OAuth state alongside expired storage data. + pub fn with_oauth_state_manager(mut self, manager: Arc) -> Self { + self.handlers.insert( + "cleanup_plugin_data".to_string(), + Arc::new(CleanupPluginDataHandler::new().with_oauth_state_manager(manager)), + ); + self + } + /// Set the settings service for runtime configuration /// /// This also registers/updates handlers that depend on settings: @@ -200,6 +219,18 @@ impl TaskWorker { } self.handlers .insert("plugin_auto_match".to_string(), Arc::new(handler)); + // Register user plugin sync handler + self.handlers.insert( + "user_plugin_sync".to_string(), + Arc::new(UserPluginSyncHandler::new(plugin_manager.clone())), + ); + // Register user plugin recommendations handler + self.handlers.insert( + "user_plugin_recommendations".to_string(), + Arc::new(UserPluginRecommendationsHandler::new( + plugin_manager.clone(), + )), + ); self.plugin_manager = Some(plugin_manager); self } diff --git a/tests/api.rs b/tests/api.rs index 87aa0f84..e2905a6b 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -31,6 +31,7 @@ mod api { mod plugins; mod rate_limit; mod read_progress; + mod recommendations; mod scan; mod series; mod settings; @@ -39,6 +40,7 @@ mod api { mod tags; mod task_metrics; mod thumbnails; + mod user_plugins; mod user_preferences; mod user_ratings; } diff --git a/tests/api/oidc.rs b/tests/api/oidc.rs index ef640dd3..82a315ce 100644 --- a/tests/api/oidc.rs +++ b/tests/api/oidc.rs @@ -93,6 +93,7 @@ async fn create_test_state_with_oidc( plugin_manager, plugin_metrics_service, oidc_service, + oauth_state_manager: Arc::new(codex::services::user_plugin::OAuthStateManager::new()), }) } diff --git a/tests/api/pdf_cache.rs b/tests/api/pdf_cache.rs index 8241f706..441ae2cb 100644 --- a/tests/api/pdf_cache.rs +++ b/tests/api/pdf_cache.rs @@ -88,6 +88,7 @@ async fn create_test_app_state_with_pdf_cache( plugin_manager, plugin_metrics_service, oidc_service: None, + oauth_state_manager: Arc::new(codex::services::user_plugin::OAuthStateManager::new()), }) } diff --git a/tests/api/plugins.rs b/tests/api/plugins.rs index 16b3934a..78bdccf1 100644 --- a/tests/api/plugins.rs +++ b/tests/api/plugins.rs @@ -164,7 +164,7 @@ async fn test_create_plugin_minimal() { let response = response.expect("Expected response body"); assert_eq!(response.plugin.name, "minimal-plugin"); assert_eq!(response.plugin.plugin_type, "system"); - assert_eq!(response.plugin.credential_delivery, "env"); + assert_eq!(response.plugin.credential_delivery, "init_message"); } #[tokio::test] diff --git a/tests/api/rate_limit.rs b/tests/api/rate_limit.rs index c1858bc8..3315e4a6 100644 --- a/tests/api/rate_limit.rs +++ b/tests/api/rate_limit.rs @@ -118,6 +118,7 @@ async fn create_rate_limited_app_state( plugin_manager, plugin_metrics_service, oidc_service: None, + oauth_state_manager: Arc::new(codex::services::user_plugin::OAuthStateManager::new()), }) } diff --git a/tests/api/recommendations.rs b/tests/api/recommendations.rs new file mode 100644 index 00000000..336f6a4d --- /dev/null +++ b/tests/api/recommendations.rs @@ -0,0 +1,679 @@ +//! Recommendations API endpoint tests +//! +//! Tests for recommendation endpoints: +//! - GET /api/v1/user/recommendations - Get personalized recommendations +//! - POST /api/v1/user/recommendations/refresh - Refresh recommendations +//! - POST /api/v1/user/recommendations/:external_id/dismiss - Dismiss a recommendation + +#[path = "../common/mod.rs"] +mod common; + +use common::db::setup_test_db; +use common::fixtures::create_test_user; +use common::http::{ + create_test_auth_state, create_test_router, generate_test_token, get_request, + get_request_with_auth, make_json_request, post_json_request_with_auth, post_request_with_auth, +}; +use hyper::StatusCode; +use serde_json::json; + +use codex::db::repositories::{PluginsRepository, UserPluginsRepository, UserRepository}; +use codex::utils::password; +use std::sync::Once; + +static INIT_ENCRYPTION: Once = Once::new(); + +/// Ensure encryption key is set for tests that need to store OAuth tokens +fn ensure_test_encryption_key() { + INIT_ENCRYPTION.call_once(|| { + if std::env::var("CODEX_ENCRYPTION_KEY").is_err() { + // SAFETY: This is only called once from test code in a Once block, + // before any concurrent access to this env var. + unsafe { + std::env::set_var( + "CODEX_ENCRYPTION_KEY", + "dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleTEyMzQ=", // 32 bytes + ); + } + } + }); +} + +// ============================================================================= +// Helper functions +// ============================================================================= + +/// Create a regular user and return (user_id, token) +async fn create_user_and_token( + db: &sea_orm::DatabaseConnection, + state: &codex::api::extractors::AppState, + username: &str, +) -> (uuid::Uuid, String) { + let password_hash = password::hash_password("user123").unwrap(); + let user = create_test_user( + username, + &format!("{}@example.com", username), + &password_hash, + false, + ); + let created = UserRepository::create(db, &user).await.unwrap(); + let token = generate_test_token(state, &created); + (created.id, token) +} + +/// Create a user-type plugin with a recommendation provider manifest +async fn create_recommendation_plugin( + db: &sea_orm::DatabaseConnection, + name: &str, + display_name: &str, +) -> uuid::Uuid { + let plugin = PluginsRepository::create( + db, + name, + display_name, + Some("A test recommendation plugin"), + "user", + "echo", + vec!["hello".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "none", + None, + true, + None, + None, + ) + .await + .unwrap(); + + // Set manifest with recommendation provider capability + let manifest = json!({ + "name": name, + "displayName": display_name, + "version": "1.0.0", + "protocolVersion": "1.0", + "pluginType": "user", + "capabilities": { + "userRecommendationProvider": true + }, + "userDescription": "Get personalized recommendations" + }); + PluginsRepository::update_manifest(db, plugin.id, Some(manifest)) + .await + .unwrap(); + + plugin.id +} + +/// Create a user-type plugin WITHOUT recommendation capability +async fn create_non_recommendation_plugin( + db: &sea_orm::DatabaseConnection, + name: &str, + display_name: &str, +) -> uuid::Uuid { + let plugin = PluginsRepository::create( + db, + name, + display_name, + Some("A non-recommendation plugin"), + "user", + "echo", + vec!["hello".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "none", + None, + true, + None, + None, + ) + .await + .unwrap(); + + plugin.id +} + +// ============================================================================= +// Authentication Tests +// ============================================================================= + +#[tokio::test] +async fn test_get_recommendations_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let request = get_request("/api/v1/user/recommendations"); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn test_refresh_recommendations_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let request = common::http::post_request("/api/v1/user/recommendations/refresh"); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn test_dismiss_recommendation_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let body = json!({}); + let request = + common::http::post_json_request("/api/v1/user/recommendations/12345/dismiss", &body); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +// ============================================================================= +// No Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_get_recommendations_no_plugin_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/recommendations", &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_refresh_recommendations_no_plugin_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth("/api/v1/user/recommendations/refresh", &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_dismiss_recommendations_no_plugin_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let body = json!({}); + let app = create_test_router(state.clone()).await; + let request = + post_json_request_with_auth("/api/v1/user/recommendations/12345/dismiss", &body, &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// Non-Recommendation Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_get_recommendations_non_rec_plugin_returns_404() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + // Create and enable a non-recommendation plugin + let plugin_id = create_non_recommendation_plugin(&db, "sync-only", "Sync Only Plugin").await; + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Should still return 404 since no *recommendation* plugin is enabled + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/recommendations", &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// Refresh Recommendations Tests (enqueue task) +// ============================================================================= + +#[tokio::test] +async fn test_refresh_recommendations_enqueues_task() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Simulate being connected by setting oauth tokens + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_access_token", + Some("fake_refresh_token"), + None, + None, + ) + .await + .unwrap(); + + // Refresh recommendations + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth("/api/v1/user/recommendations/refresh", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let body = response.expect("Expected response body"); + assert!(body.get("taskId").is_some()); + assert!( + body["message"] + .as_str() + .unwrap() + .contains("AniList Recommendations") + ); +} + +// ============================================================================= +// User Isolation Tests +// ============================================================================= + +#[tokio::test] +async fn test_recommendations_user_isolation() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_a_id, token_a) = create_user_and_token(&db, &state, "usera").await; + let (_, token_b) = create_user_and_token(&db, &state, "userb").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // User A enables and connects the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token_a, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_a_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_token", + Some("fake_refresh"), + None, + None, + ) + .await + .unwrap(); + + // User A can refresh + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth("/api/v1/user/recommendations/refresh", &token_a); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // User B cannot see recommendations (no plugin enabled) + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/recommendations", &token_b); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// Disabled Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_recommendations_disabled_plugin_returns_404() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Disable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/disable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Should return 404 since plugin is disabled + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/recommendations", &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// Get Recommendations Tests (connected plugin, error handling) +// ============================================================================= + +#[tokio::test] +async fn test_get_recommendations_connected_plugin_graceful_error() { + // When a recommendation plugin is enabled and "connected" (has OAuth tokens), + // but the plugin process can't actually be spawned (because the command is "echo"), + // the endpoint should return a 500 error (not crash) with a meaningful message. + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Simulate being connected by setting oauth tokens + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_access_token", + Some("fake_refresh_token"), + None, + None, + ) + .await + .unwrap(); + + // GET recommendations — plugin process will fail to spawn (command is "echo") + // but the endpoint should handle this gracefully + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/recommendations", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + // Should return 500 with an error message, not panic or hang + assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); + let body = response.expect("Expected error response body"); + assert!(body.get("message").is_some()); +} + +#[tokio::test] +async fn test_get_recommendations_enabled_but_not_connected() { + // When a recommendation plugin is enabled but not connected (no OAuth tokens), + // the GET endpoint should still attempt to call the plugin. + // This tests that the API layer doesn't require connected status for GET. + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // Enable the plugin but don't set OAuth tokens + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // GET recommendations — should fail gracefully (plugin spawn will fail) + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/recommendations", &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + // Plugin spawn will fail, should get 500 not a panic + assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); +} + +// ============================================================================= +// Dismiss Recommendation Tests +// ============================================================================= + +#[tokio::test] +async fn test_dismiss_recommendation_connected_plugin_graceful_error() { + // When a recommendation plugin is enabled and connected, but the plugin + // process can't actually be spawned, dismiss should return an error gracefully. + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // Enable and connect the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_access_token", + Some("fake_refresh_token"), + None, + None, + ) + .await + .unwrap(); + + // Dismiss a recommendation — plugin will fail to spawn + let body = json!({"reason": "not_interested"}); + let app = create_test_router(state.clone()).await; + let request = + post_json_request_with_auth("/api/v1/user/recommendations/12345/dismiss", &body, &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + // Should return 500 with error message, not panic + assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); + let body = response.expect("Expected error response body"); + assert!(body.get("message").is_some()); +} + +#[tokio::test] +async fn test_dismiss_recommendation_without_reason() { + // Dismiss should accept an empty body (reason is optional) + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + // No plugin enabled — should return 404, but validates the request is accepted + let body = json!({}); + let app = create_test_router(state.clone()).await; + let request = + post_json_request_with_auth("/api/v1/user/recommendations/12345/dismiss", &body, &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_dismiss_recommendation_various_reasons() { + // Verify all valid reason strings are accepted by the endpoint + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + for reason in &["not_interested", "already_read", "already_owned"] { + let body = json!({"reason": reason}); + let app = create_test_router(state.clone()).await; + let request = post_json_request_with_auth( + "/api/v1/user/recommendations/test-id/dismiss", + &body, + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + // Will be 404 since no plugin enabled, but validates request parsing + assert_eq!( + status, + StatusCode::NOT_FOUND, + "reason '{}' should be accepted", + reason + ); + } +} + +// ============================================================================= +// Task Deduplication Tests +// ============================================================================= + +#[tokio::test] +async fn test_refresh_recommendations_deduplication() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = + create_recommendation_plugin(&db, "recommendations-anilist", "AniList Recommendations") + .await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Simulate being connected by setting oauth tokens + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_access_token", + Some("fake_refresh_token"), + None, + None, + ) + .await + .unwrap(); + + // First refresh — should succeed + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth("/api/v1/user/recommendations/refresh", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + let body = response.expect("Expected response body"); + assert!(body.get("taskId").is_some()); + + // Second refresh — should return 409 Conflict + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth("/api/v1/user/recommendations/refresh", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::CONFLICT); + let body = response.expect("Expected error body"); + assert_eq!( + body["message"], + "Recommendation refresh already in progress" + ); +} diff --git a/tests/api/task_metrics.rs b/tests/api/task_metrics.rs index 8bb632df..16d767f2 100644 --- a/tests/api/task_metrics.rs +++ b/tests/api/task_metrics.rs @@ -84,6 +84,7 @@ async fn create_test_app_state_with_metrics(db: DatabaseConnection) -> Arc (uuid::Uuid, String) { + let password_hash = password::hash_password("user123").unwrap(); + let user = create_test_user( + username, + &format!("{}@example.com", username), + &password_hash, + false, + ); + let created = UserRepository::create(db, &user).await.unwrap(); + let token = generate_test_token(state, &created); + (created.id, token) +} + +/// Create a user-type plugin (admin operation) and return its ID +async fn create_user_type_plugin( + db: &sea_orm::DatabaseConnection, + name: &str, + display_name: &str, +) -> uuid::Uuid { + let plugin = PluginsRepository::create( + db, + name, + display_name, + Some("A test user plugin"), + "user", // plugin_type + "echo", // command + vec!["hello".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "none", + None, + true, // enabled + None, + None, + ) + .await + .unwrap(); + plugin.id +} + +/// Create a user-type plugin with a sync provider manifest +async fn create_sync_plugin( + db: &sea_orm::DatabaseConnection, + name: &str, + display_name: &str, +) -> uuid::Uuid { + let plugin = PluginsRepository::create( + db, + name, + display_name, + Some("A test sync plugin"), + "user", + "echo", + vec!["hello".to_string()], + vec![], + None, + vec![], + vec![], + vec![], + None, + "none", + None, + true, + None, + None, + ) + .await + .unwrap(); + + // Set manifest with sync provider capability + let manifest = json!({ + "name": name, + "displayName": display_name, + "version": "1.0.0", + "protocolVersion": "1.0", + "pluginType": "user", + "capabilities": { + "userReadSync": true + }, + "userDescription": "Sync reading progress" + }); + PluginsRepository::update_manifest(db, plugin.id, Some(manifest)) + .await + .unwrap(); + + plugin.id +} + +/// Create a system-type plugin (admin operation) and return its ID +async fn create_system_type_plugin(db: &sea_orm::DatabaseConnection, name: &str) -> uuid::Uuid { + let plugin = PluginsRepository::create( + db, + name, + name, + None, + "system", // plugin_type + "echo", + vec![], + vec![], + None, + vec![], + vec![], + vec![], + None, + "none", + None, + true, + None, + None, + ) + .await + .unwrap(); + plugin.id +} + +// ============================================================================= +// Authentication Tests +// ============================================================================= + +#[tokio::test] +async fn test_list_user_plugins_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let request = get_request("/api/v1/user/plugins"); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn test_get_user_plugin_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let fake_id = uuid::Uuid::new_v4(); + let request = get_request(&format!("/api/v1/user/plugins/{}", fake_id)); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +// ============================================================================= +// List User Plugins Tests +// ============================================================================= + +#[tokio::test] +async fn test_list_user_plugins_empty() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let request = get_request_with_auth("/api/v1/user/plugins", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let response = response.expect("Expected response body"); + assert!(response.enabled.is_empty()); + assert!(response.available.is_empty()); +} + +#[tokio::test] +async fn test_list_user_plugins_shows_available() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + // Create a user-type plugin + create_user_type_plugin(&db, "test-sync", "Test Sync Plugin").await; + + // System plugins should NOT appear in available + create_system_type_plugin(&db, "system-plugin").await; + + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/plugins", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let response = response.expect("Expected response body"); + assert!(response.enabled.is_empty()); + assert_eq!(response.available.len(), 1); + assert_eq!(response.available[0].name, "test-sync"); + assert_eq!(response.available[0].display_name, "Test Sync Plugin"); +} + +#[tokio::test] +async fn test_list_user_plugins_shows_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync Plugin").await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // List should show it in enabled, not available + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/plugins", &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let response = response.expect("Expected response body"); + assert_eq!(response.enabled.len(), 1); + assert_eq!(response.enabled[0].plugin_name, "test-sync"); + assert!(response.available.is_empty()); +} + +// ============================================================================= +// Enable Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_enable_plugin_success() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let dto = response.expect("Expected response body"); + assert_eq!(dto.plugin_id, plugin_id); + assert_eq!(dto.plugin_name, "test-sync"); + assert!(dto.enabled); + assert!(!dto.connected); // No OAuth yet +} + +#[tokio::test] +async fn test_enable_plugin_not_found() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let fake_id = uuid::Uuid::new_v4(); + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/enable", fake_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_enable_system_plugin_rejected() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let system_id = create_system_type_plugin(&db, "system-plugin").await; + + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", system_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_enable_plugin_duplicate_rejected() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Enable first time + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Enable second time - should conflict + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::CONFLICT); +} + +// ============================================================================= +// Get User Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_get_user_plugin_success() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Enable first + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Get the plugin + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth(&format!("/api/v1/user/plugins/{}", plugin_id), &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let dto = response.expect("Expected response body"); + assert_eq!(dto.plugin_id, plugin_id); + assert_eq!(dto.plugin_name, "test-sync"); + assert_eq!(dto.plugin_display_name, "Test Sync"); + assert!(dto.enabled); + assert!(!dto.connected); + assert_eq!(dto.health_status, "unknown"); +} + +#[tokio::test] +async fn test_get_user_plugin_not_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Try to get without enabling + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth(&format!("/api/v1/user/plugins/{}", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_get_user_plugin_isolation() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token_a) = create_user_and_token(&db, &state, "usera").await; + let (_, token_b) = create_user_and_token(&db, &state, "userb").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // User A enables plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token_a, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // User B cannot see User A's plugin instance + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth(&format!("/api/v1/user/plugins/{}", plugin_id), &token_b); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// Disable Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_disable_plugin_success() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Enable + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Disable + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/disable", plugin_id), + &token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let body = response.expect("Expected response body"); + assert_eq!(body["success"], true); +} + +#[tokio::test] +async fn test_disable_plugin_not_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/disable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// Update Config Tests +// ============================================================================= + +#[tokio::test] +async fn test_update_config_success() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Enable first + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Update config + let config_body = json!({ "config": { "autoSync": true, "syncInterval": 3600 } }); + let app = create_test_router(state.clone()).await; + let request = patch_json_request_with_auth( + &format!("/api/v1/user/plugins/{}/config", plugin_id), + &config_body, + &token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let dto = response.expect("Expected response body"); + assert_eq!(dto.config["autoSync"], true); + assert_eq!(dto.config["syncInterval"], 3600); +} + +#[tokio::test] +async fn test_update_config_not_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + let config_body = json!({ "config": { "autoSync": true } }); + let app = create_test_router(state.clone()).await; + let request = patch_json_request_with_auth( + &format!("/api/v1/user/plugins/{}/config", plugin_id), + &config_body, + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_update_config_invalid_not_object() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Enable first + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Try to set config as an array (invalid) + let config_body = json!({ "config": [1, 2, 3] }); + let app = create_test_router(state.clone()).await; + let request = patch_json_request_with_auth( + &format!("/api/v1/user/plugins/{}/config", plugin_id), + &config_body, + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); +} + +// ============================================================================= +// Disconnect Plugin Tests +// ============================================================================= + +#[tokio::test] +async fn test_disconnect_plugin_success() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // Enable + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Disconnect + let app = create_test_router(state.clone()).await; + let request = delete_request_with_auth(&format!("/api/v1/user/plugins/{}", plugin_id), &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let body = response.expect("Expected response body"); + assert_eq!(body["success"], true); + + // Verify plugin no longer accessible + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth(&format!("/api/v1/user/plugins/{}", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_disconnect_plugin_not_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + let app = create_test_router(state.clone()).await; + let request = delete_request_with_auth(&format!("/api/v1/user/plugins/{}", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +// ============================================================================= +// User Isolation Tests +// ============================================================================= + +#[tokio::test] +async fn test_user_plugin_isolation_between_users() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token_a) = create_user_and_token(&db, &state, "usera").await; + let (_, token_b) = create_user_and_token(&db, &state, "userb").await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + // User A enables the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token_a, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // User B's list should show plugin as available (not enabled) + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/plugins", &token_b); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let response = response.expect("Expected response body"); + assert!(response.enabled.is_empty()); + assert_eq!(response.available.len(), 1); + assert_eq!(response.available[0].plugin_id, plugin_id); + + // User B can also enable independently + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token_b, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Now both users have it enabled + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/plugins", &token_a); + let (_, response_a): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(response_a.unwrap().enabled.len(), 1); + + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth("/api/v1/user/plugins", &token_b); + let (_, response_b): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(response_b.unwrap().enabled.len(), 1); +} + +// ============================================================================= +// Admin can also use user plugin endpoints (admin is a user too) +// ============================================================================= + +#[tokio::test] +async fn test_admin_can_enable_user_plugins() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let admin_token = create_admin_and_token(&db, &state).await; + + let plugin_id = create_user_type_plugin(&db, "test-sync", "Test Sync").await; + + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &admin_token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let dto = response.expect("Expected response body"); + assert_eq!(dto.plugin_id, plugin_id); + assert!(dto.enabled); +} + +// ============================================================================= +// Sync Trigger Tests +// ============================================================================= + +#[tokio::test] +async fn test_trigger_sync_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let fake_id = uuid::Uuid::new_v4(); + let request = common::http::post_request(&format!("/api/v1/user/plugins/{}/sync", fake_id)); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn test_trigger_sync_not_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Try to sync without enabling + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_trigger_sync_not_sync_provider() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + // Create a user plugin WITHOUT sync capability + let plugin_id = create_user_type_plugin(&db, "no-sync", "No Sync Plugin").await; + + // Enable it + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Fake authentication by setting oauth tokens directly + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_token", + Some("fake_refresh"), + None, + None, + ) + .await + .unwrap(); + + // Try to sync - should fail because plugin is not a sync provider + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_trigger_sync_not_connected() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable but don't connect (no OAuth) + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Try to sync without being connected + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn test_trigger_sync_success() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Simulate being connected by setting oauth tokens + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_access_token", + Some("fake_refresh_token"), + None, + None, + ) + .await + .unwrap(); + + // Trigger sync + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let resp = response.expect("Expected response body"); + assert!(!resp.task_id.is_nil()); + assert!(resp.message.contains("AniList Sync")); +} + +#[tokio::test] +async fn test_trigger_sync_disabled_plugin() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Set tokens + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_token", + Some("fake_refresh"), + None, + None, + ) + .await + .unwrap(); + + // Disable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/disable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Try to sync with disabled plugin + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); +} + +// ============================================================================= +// Sync Status Tests +// ============================================================================= + +#[tokio::test] +async fn test_sync_status_requires_auth() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let app = create_test_router(state.clone()).await; + + let fake_id = uuid::Uuid::new_v4(); + let request = get_request(&format!("/api/v1/user/plugins/{}/sync/status", fake_id)); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn test_sync_status_not_enabled() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync/status", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_sync_status_success() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Get status + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync/status", plugin_id), + &token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let dto = response.expect("Expected response body"); + assert_eq!(dto.plugin_id, plugin_id); + assert_eq!(dto.plugin_name, "AniList Sync"); + assert!(!dto.connected); // No OAuth yet + assert!(dto.last_sync_at.is_none()); + assert_eq!(dto.health_status, "unknown"); + assert_eq!(dto.failure_count, 0); + assert!(dto.enabled); +} + +#[tokio::test] +async fn test_sync_status_isolation() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token_a) = create_user_and_token(&db, &state, "usera").await; + let (_, token_b) = create_user_and_token(&db, &state, "userb").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // User A enables + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token_a, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // User B cannot see User A's sync status + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync/status", plugin_id), + &token_b, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn test_sync_status_default_has_no_live_fields() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Get status without ?live=true + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync/status", plugin_id), + &token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + assert_eq!(status, StatusCode::OK); + let body = response.expect("Expected response body"); + + // DB fields present + assert!(body.get("pluginId").is_some()); + assert!(body.get("pluginName").is_some()); + + // Live fields absent (not null, truly absent from JSON) + assert!(body.get("externalCount").is_none()); + assert!(body.get("pendingPush").is_none()); + assert!(body.get("pendingPull").is_none()); + assert!(body.get("conflicts").is_none()); + assert!(body.get("liveError").is_none()); +} + +#[tokio::test] +async fn test_sync_status_live_degrades_gracefully() { + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (_, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Get status with ?live=true (no real plugin process available) + let app = create_test_router(state.clone()).await; + let request = get_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync/status?live=true", plugin_id), + &token, + ); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + + // Should still succeed with 200 (graceful degradation) + assert_eq!(status, StatusCode::OK); + let body = response.expect("Expected response body"); + + // DB fields still present + assert_eq!(body["pluginName"], "AniList Sync"); + assert_eq!(body["enabled"], true); + + // Live data fields absent due to error + assert!(body.get("externalCount").is_none()); + assert!(body.get("pendingPush").is_none()); + assert!(body.get("pendingPull").is_none()); + assert!(body.get("conflicts").is_none()); + + // live_error should be present explaining why + assert!(body.get("liveError").is_some()); + let error_msg = body["liveError"].as_str().unwrap(); + assert!(!error_msg.is_empty()); +} + +// ============================================================================= +// Task Deduplication Tests +// ============================================================================= + +#[tokio::test] +async fn test_trigger_sync_deduplication() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_id, token) = create_user_and_token(&db, &state, "testuser").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable the plugin + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/enable", plugin_id), + &token, + ); + let (status, _): (StatusCode, Option) = make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Simulate being connected by setting oauth tokens + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_access_token", + Some("fake_refresh_token"), + None, + None, + ) + .await + .unwrap(); + + // First sync trigger — should succeed + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + let resp = response.expect("Expected response body"); + assert!(!resp.task_id.is_nil()); + + // Second sync trigger — should return 409 Conflict + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/sync", plugin_id), &token); + let (status, response): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::CONFLICT); + let body = response.expect("Expected error body"); + assert_eq!(body["message"], "Sync already in progress"); +} + +#[tokio::test] +async fn test_trigger_sync_deduplication_different_users() { + ensure_test_encryption_key(); + let (db, _temp_dir) = setup_test_db().await; + let state = create_test_auth_state(db.clone()).await; + let (user_a_id, token_a) = create_user_and_token(&db, &state, "usera").await; + let (user_b_id, token_b) = create_user_and_token(&db, &state, "userb").await; + + let plugin_id = create_sync_plugin(&db, "sync-anilist", "AniList Sync").await; + + // Enable plugin for both users + for (user_id, token) in [(user_a_id, &token_a), (user_b_id, &token_b)] { + let app = create_test_router(state.clone()).await; + let request = + post_request_with_auth(&format!("/api/v1/user/plugins/{}/enable", plugin_id), token); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // Set credentials + let instance = UserPluginsRepository::get_by_user_and_plugin(&db, user_id, plugin_id) + .await + .unwrap() + .unwrap(); + UserPluginsRepository::update_oauth_tokens( + &db, + instance.id, + "fake_token", + Some("fake_refresh"), + None, + None, + ) + .await + .unwrap(); + } + + // User A triggers sync + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync", plugin_id), + &token_a, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); + + // User B triggers sync — should also succeed (different user) + let app = create_test_router(state.clone()).await; + let request = post_request_with_auth( + &format!("/api/v1/user/plugins/{}/sync", plugin_id), + &token_b, + ); + let (status, _): (StatusCode, Option) = + make_json_request(app, request).await; + assert_eq!(status, StatusCode::OK); +} diff --git a/tests/common/http.rs b/tests/common/http.rs index bab0b682..a4413add 100644 --- a/tests/common/http.rs +++ b/tests/common/http.rs @@ -68,6 +68,7 @@ pub async fn create_test_auth_state(db: DatabaseConnection) -> Arc { plugin_manager, plugin_metrics_service, oidc_service: None, // Tests disable OIDC by default + oauth_state_manager: Arc::new(codex::services::user_plugin::OAuthStateManager::new()), }) } @@ -119,6 +120,7 @@ pub async fn create_test_app_state(db: DatabaseConnection) -> Arc { plugin_manager, plugin_metrics_service, oidc_service: None, // Tests disable OIDC by default + oauth_state_manager: Arc::new(codex::services::user_plugin::OAuthStateManager::new()), }) } @@ -195,6 +197,7 @@ pub async fn create_test_router(state: Arc) -> Router { plugin_manager, plugin_metrics_service, oidc_service: None, // Tests disable OIDC by default + oauth_state_manager: Arc::new(codex::services::user_plugin::OAuthStateManager::new()), }); let config = create_test_config(); create_router(app_state, &config) diff --git a/tests/services/metadata_apply.rs b/tests/services/metadata_apply.rs index 6b698055..cec39d03 100644 --- a/tests/services/metadata_apply.rs +++ b/tests/services/metadata_apply.rs @@ -80,6 +80,7 @@ fn create_test_metadata(title: &str) -> PluginSeriesMetadata { rating: None, external_ratings: vec![], external_links: vec![], + external_ids: vec![], } } diff --git a/tests/task_queue.rs b/tests/task_queue.rs index e756be7c..45bd0837 100644 --- a/tests/task_queue.rs +++ b/tests/task_queue.rs @@ -2109,3 +2109,210 @@ async fn test_plugin_rate_limit_none_means_unlimited() { ); } } + +// ============================================================================ +// has_pending_or_processing tests (database-level JSON filtering) +// ============================================================================ + +/// Test that has_pending_or_processing returns false when no tasks exist +#[tokio::test] +async fn test_has_pending_or_processing_no_tasks() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + let result = + TaskRepository::has_pending_or_processing(&db, "user_plugin_sync", plugin_id, user_id) + .await + .expect("Failed to check for pending tasks"); + + assert!(!result, "Should return false when no tasks exist"); +} + +/// Test that has_pending_or_processing detects a pending task with matching params +#[tokio::test] +async fn test_has_pending_or_processing_finds_pending_task() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + // Enqueue a sync task + let task_type = TaskType::UserPluginSync { plugin_id, user_id }; + TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + let result = + TaskRepository::has_pending_or_processing(&db, "user_plugin_sync", plugin_id, user_id) + .await + .expect("Failed to check for pending tasks"); + + assert!(result, "Should find the pending sync task"); +} + +/// Test that has_pending_or_processing detects a processing task with matching params +#[tokio::test] +async fn test_has_pending_or_processing_finds_processing_task() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + // Enqueue and claim (transitions to processing) + let task_type = TaskType::UserPluginSync { plugin_id, user_id }; + TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + TaskRepository::claim_next(&db, "worker-1", 300, false) + .await + .expect("Failed to claim task"); + + let result = + TaskRepository::has_pending_or_processing(&db, "user_plugin_sync", plugin_id, user_id) + .await + .expect("Failed to check for pending tasks"); + + assert!(result, "Should find the processing sync task"); +} + +/// Test that has_pending_or_processing ignores completed tasks +#[tokio::test] +async fn test_has_pending_or_processing_ignores_completed_tasks() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + // Enqueue, claim, and complete + let task_type = TaskType::UserPluginSync { plugin_id, user_id }; + let task_id = TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + TaskRepository::claim_next(&db, "worker-1", 300, false) + .await + .expect("Failed to claim task"); + + TaskRepository::mark_completed(&db, task_id, None) + .await + .expect("Failed to mark completed"); + + let result = + TaskRepository::has_pending_or_processing(&db, "user_plugin_sync", plugin_id, user_id) + .await + .expect("Failed to check for pending tasks"); + + assert!(!result, "Should not find the completed task"); +} + +/// Test that has_pending_or_processing does not match different plugin_id +#[tokio::test] +async fn test_has_pending_or_processing_different_plugin_id() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let other_plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + // Enqueue task for one plugin + let task_type = TaskType::UserPluginSync { plugin_id, user_id }; + TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + // Check with a different plugin_id + let result = TaskRepository::has_pending_or_processing( + &db, + "user_plugin_sync", + other_plugin_id, + user_id, + ) + .await + .expect("Failed to check for pending tasks"); + + assert!(!result, "Should not match task with different plugin_id"); +} + +/// Test that has_pending_or_processing does not match different user_id +#[tokio::test] +async fn test_has_pending_or_processing_different_user_id() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + let other_user_id = Uuid::new_v4(); + + // Enqueue task for one user + let task_type = TaskType::UserPluginSync { plugin_id, user_id }; + TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + // Check with a different user_id + let result = TaskRepository::has_pending_or_processing( + &db, + "user_plugin_sync", + plugin_id, + other_user_id, + ) + .await + .expect("Failed to check for pending tasks"); + + assert!(!result, "Should not match task with different user_id"); +} + +/// Test that has_pending_or_processing does not match different task_type +#[tokio::test] +async fn test_has_pending_or_processing_different_task_type() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + // Enqueue a sync task + let task_type = TaskType::UserPluginSync { plugin_id, user_id }; + TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + // Check for recommendations type instead + let result = TaskRepository::has_pending_or_processing( + &db, + "user_plugin_recommendations", + plugin_id, + user_id, + ) + .await + .expect("Failed to check for pending tasks"); + + assert!(!result, "Should not match different task_type"); +} + +/// Test has_pending_or_processing with recommendations task type +#[tokio::test] +async fn test_has_pending_or_processing_recommendations_task() { + let (db, _temp_dir) = setup_test_db().await; + + let plugin_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + // Enqueue a recommendations task + let task_type = TaskType::UserPluginRecommendations { plugin_id, user_id }; + TaskRepository::enqueue(&db, task_type, 0, None) + .await + .expect("Failed to enqueue task"); + + let result = TaskRepository::has_pending_or_processing( + &db, + "user_plugin_recommendations", + plugin_id, + user_id, + ) + .await + .expect("Failed to check for pending tasks"); + + assert!(result, "Should find the pending recommendations task"); +} diff --git a/web/openapi.json b/web/openapi.json index e0f52f89..96e4076a 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -11439,6 +11439,457 @@ ] } }, + "/api/v1/user/plugins": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "List user's plugins (enabled and available)", + "description": "Returns both plugins the user has enabled and plugins available for them to enable.", + "operationId": "list_user_plugins", + "responses": { + "200": { + "description": "User plugins list", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginsListResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + } + } + } + }, + "/api/v1/user/plugins/oauth/callback": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "Handle OAuth callback from external provider", + "description": "This endpoint receives the callback after the user authenticates with the\nexternal service. It exchanges the authorization code for tokens and stores\nthem encrypted in the database.", + "operationId": "oauth_callback", + "parameters": [ + { + "name": "code", + "in": "query", + "description": "Authorization code from OAuth provider", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "state", + "in": "query", + "description": "State parameter for CSRF protection", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "HTML page that auto-closes the popup" + }, + "400": { + "description": "Invalid callback parameters" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "Get a single user plugin instance", + "description": "Returns detailed status for a plugin the user has enabled.", + "operationId": "get_user_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User plugin details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + }, + "delete": { + "tags": [ + "User Plugins" + ], + "summary": "Disconnect a plugin (remove data and credentials)", + "operationId": "disconnect_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to disconnect", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin disconnected and data removed" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/config": { + "patch": { + "tags": [ + "User Plugins" + ], + "summary": "Update user plugin configuration", + "description": "Allows the user to set per-user configuration overrides for their plugin instance.", + "operationId": "update_user_plugin_config", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to update config for", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserPluginConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Configuration updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "400": { + "description": "Invalid configuration" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/credentials": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Set user credentials (personal access token) for a plugin", + "description": "Allows users to authenticate by pasting a personal access token\ninstead of going through the OAuth flow.", + "operationId": "set_user_credentials", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to set credentials for", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetUserCredentialsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Credentials stored", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/disable": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Disable a plugin for the current user", + "operationId": "disable_user_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to disable", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin disabled" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/enable": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Enable a plugin for the current user", + "operationId": "enable_user_plugin", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to enable", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Plugin enabled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPluginDto" + } + } + } + }, + "400": { + "description": "Plugin is not a user plugin or not available" + }, + "401": { + "description": "Not authenticated" + }, + "409": { + "description": "Plugin already enabled for this user" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/oauth/start": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Start OAuth flow for a user plugin", + "description": "Generates an authorization URL and returns it to the client.\nThe client should open this URL in a popup or redirect the user.", + "operationId": "oauth_start", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to start OAuth for", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OAuth authorization URL generated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthStartResponse" + } + } + } + }, + "400": { + "description": "Plugin does not support OAuth or not configured" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not found or not enabled" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/sync": { + "post": { + "tags": [ + "User Plugins" + ], + "summary": "Trigger a sync operation for a user plugin", + "description": "Enqueues a background sync task that will push/pull reading progress\nbetween Codex and the external service.", + "operationId": "trigger_sync", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to sync", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Sync task enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncTriggerResponse" + } + } + } + }, + "400": { + "description": "Plugin is not a sync provider or not connected" + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + }, + "409": { + "description": "Sync already in progress" + } + } + } + }, + "/api/v1/user/plugins/{plugin_id}/sync/status": { + "get": { + "tags": [ + "User Plugins" + ], + "summary": "Get sync status for a user plugin", + "description": "Returns the current sync status including last sync time, health, and failure count.\nPass `?live=true` to also query the plugin process for live sync state (pending push/pull,\nconflicts, external entry count). This spawns the plugin process and is more expensive.", + "operationId": "get_sync_status", + "parameters": [ + { + "name": "plugin_id", + "in": "path", + "description": "Plugin ID to check sync status", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "live", + "in": "query", + "description": "If true, spawn the plugin process and query live sync state\n(external count, pending push/pull, conflicts).\nDefault: false (returns database-stored metadata only).", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Sync status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncStatusDto" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "Plugin not enabled for this user" + } + } + } + }, "/api/v1/user/preferences": { "get": { "tags": [ @@ -11687,6 +12138,114 @@ ] } }, + "/api/v1/user/recommendations": { + "get": { + "tags": [ + "Recommendations" + ], + "summary": "Get personalized recommendations", + "description": "Returns recommendations from the user's enabled recommendation plugin.\nThe plugin may return cached results or generate fresh recommendations.", + "operationId": "get_recommendations", + "responses": { + "200": { + "description": "Personalized recommendations", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecommendationsResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "No recommendation plugin enabled" + } + } + } + }, + "/api/v1/user/recommendations/refresh": { + "post": { + "tags": [ + "Recommendations" + ], + "summary": "Refresh recommendations", + "description": "Enqueues a background task to regenerate recommendations by clearing\nthe cache and updating the taste profile.", + "operationId": "refresh_recommendations", + "responses": { + "200": { + "description": "Refresh task enqueued", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecommendationsRefreshResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "No recommendation plugin enabled" + }, + "409": { + "description": "Recommendation refresh already in progress" + } + } + } + }, + "/api/v1/user/recommendations/{external_id}/dismiss": { + "post": { + "tags": [ + "Recommendations" + ], + "summary": "Dismiss a recommendation", + "description": "Tells the recommendation plugin that the user is not interested in a\nparticular recommendation, so it can be excluded from future results.", + "operationId": "dismiss_recommendation", + "parameters": [ + { + "name": "external_id", + "in": "path", + "description": "External ID of the recommendation to dismiss", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissRecommendationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Recommendation dismissed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissRecommendationResponse" + } + } + } + }, + "401": { + "description": "Not authenticated" + }, + "404": { + "description": "No recommendation plugin enabled" + } + } + } + }, "/api/v1/user/sharing-tags": { "get": { "tags": [ @@ -15107,6 +15666,59 @@ } } }, + "AvailablePluginDto": { + "type": "object", + "description": "Available plugin (not yet enabled by user)", + "required": [ + "pluginId", + "name", + "displayName", + "requiresOauth", + "oauthConfigured", + "capabilities" + ], + "properties": { + "capabilities": { + "$ref": "#/components/schemas/UserPluginCapabilitiesDto", + "description": "Plugin capabilities" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Plugin description" + }, + "displayName": { + "type": "string", + "description": "Plugin display name" + }, + "name": { + "type": "string", + "description": "Plugin name" + }, + "oauthConfigured": { + "type": "boolean", + "description": "Whether the admin has configured OAuth credentials (client_id set)" + }, + "pluginId": { + "type": "string", + "format": "uuid", + "description": "Plugin definition ID" + }, + "requiresOauth": { + "type": "boolean", + "description": "Whether this plugin requires OAuth authentication" + }, + "userSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "User-facing setup instructions for the plugin" + } + } + }, "BelongsTo": { "type": "object", "description": "Series membership information", @@ -18428,6 +19040,32 @@ } } }, + "DismissRecommendationRequest": { + "type": "object", + "description": "Request body for POST /api/v1/user/recommendations/:id/dismiss", + "properties": { + "reason": { + "type": [ + "string", + "null" + ], + "description": "Reason for dismissal" + } + } + }, + "DismissRecommendationResponse": { + "type": "object", + "description": "Response from POST /api/v1/user/recommendations/:id/dismiss", + "required": [ + "dismissed" + ], + "properties": { + "dismissed": { + "type": "boolean", + "description": "Whether the dismissal was recorded" + } + } + }, "DuplicateGroup": { "type": "object", "description": "A group of duplicate books", @@ -23245,6 +23883,57 @@ "smart" ] }, + "OAuthConfigDto": { + "type": "object", + "description": "OAuth 2.0 configuration from plugin manifest", + "required": [ + "authorizationUrl", + "tokenUrl", + "pkce" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "description": "OAuth 2.0 authorization endpoint URL" + }, + "pkce": { + "type": "boolean", + "description": "Whether to use PKCE (Proof Key for Code Exchange)" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required OAuth scopes" + }, + "tokenUrl": { + "type": "string", + "description": "OAuth 2.0 token endpoint URL" + }, + "userInfoUrl": { + "type": [ + "string", + "null" + ], + "description": "Optional user info endpoint URL" + } + } + }, + "OAuthStartResponse": { + "type": "object", + "description": "OAuth initiation response", + "required": [ + "redirectUrl" + ], + "properties": { + "redirectUrl": { + "type": "string", + "description": "The URL to redirect the user to for OAuth authorization", + "example": "https://anilist.co/api/v2/oauth/authorize?response_type=code&client_id=..." + } + } + }, "OidcCallbackResponse": { "type": "object", "description": "Response from OIDC callback (successful authentication)\n\nThis mirrors the standard LoginResponse format for consistency.", @@ -25462,6 +26151,13 @@ "type": "object", "description": "Plugin capabilities", "properties": { + "externalIdSource": { + "type": [ + "string", + "null" + ], + "description": "External ID source for matching sync entries to series (e.g., \"api:anilist\")" + }, "metadataProvider": { "type": "array", "items": { @@ -25469,9 +26165,13 @@ }, "description": "Content types this plugin can provide metadata for (e.g., [\"series\", \"book\"])" }, - "userSyncProvider": { + "userReadSync": { "type": "boolean", "description": "Can sync user reading progress" + }, + "userRecommendationProvider": { + "type": "boolean", + "description": "Can provide personalized recommendations" } } }, @@ -25692,6 +26392,16 @@ "description": "Whether to skip search when external ID exists for this plugin", "example": true }, + "userCount": { + "type": [ + "integer", + "null" + ], + "format": "int64", + "description": "Number of users who have enabled this plugin (only for user-type plugins)", + "example": 3, + "minimum": 0 + }, "workingDirectory": { "type": [ "string", @@ -25882,6 +26592,13 @@ "contentTypes" ], "properties": { + "adminSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "Admin-facing setup instructions (e.g., how to create OAuth app, set client ID)" + }, "author": { "type": [ "string", @@ -25933,6 +26650,17 @@ "type": "string", "description": "Unique identifier" }, + "oauth": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OAuthConfigDto", + "description": "OAuth 2.0 configuration (if plugin supports OAuth)" + } + ] + }, "protocolVersion": { "type": "string", "description": "Protocol version" @@ -25951,6 +26679,13 @@ }, "description": "Supported scopes" }, + "userSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "User-facing setup instructions (e.g., how to connect or get a personal token)" + }, "version": { "type": "string", "description": "Semantic version" @@ -26892,6 +27627,138 @@ } } }, + "RecommendationDto": { + "type": "object", + "description": "A single recommendation for the user", + "required": [ + "externalId", + "title", + "score", + "reason" + ], + "properties": { + "basedOn": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Titles that influenced this recommendation" + }, + "codexSeriesId": { + "type": [ + "string", + "null" + ], + "description": "Codex series ID if matched to an existing series" + }, + "coverUrl": { + "type": [ + "string", + "null" + ], + "description": "Cover image URL" + }, + "externalId": { + "type": "string", + "description": "External ID on the source service" + }, + "externalUrl": { + "type": [ + "string", + "null" + ], + "description": "URL to the entry on the external service" + }, + "genres": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Genres" + }, + "inLibrary": { + "type": "boolean", + "description": "Whether this series is already in the user's library" + }, + "reason": { + "type": "string", + "description": "Human-readable reason for this recommendation" + }, + "score": { + "type": "number", + "format": "double", + "description": "Confidence/relevance score (0.0 to 1.0)" + }, + "summary": { + "type": [ + "string", + "null" + ], + "description": "Summary/description" + }, + "title": { + "type": "string", + "description": "Title of the recommended series/book" + } + } + }, + "RecommendationsRefreshResponse": { + "type": "object", + "description": "Response from POST /api/v1/user/recommendations/refresh", + "required": [ + "taskId", + "message" + ], + "properties": { + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "taskId": { + "type": "string", + "format": "uuid", + "description": "Task ID for tracking the refresh operation" + } + } + }, + "RecommendationsResponse": { + "type": "object", + "description": "Response from GET /api/v1/user/recommendations", + "required": [ + "recommendations", + "pluginId", + "pluginName" + ], + "properties": { + "cached": { + "type": "boolean", + "description": "Whether these are cached results" + }, + "generatedAt": { + "type": [ + "string", + "null" + ], + "description": "When these recommendations were generated" + }, + "pluginId": { + "type": "string", + "format": "uuid", + "description": "Plugin that provided these recommendations" + }, + "pluginName": { + "type": "string", + "description": "Plugin display name" + }, + "recommendations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecommendationDto" + }, + "description": "Personalized recommendations" + } + } + }, "RegisterRequest": { "type": "object", "description": "Register request", @@ -28836,6 +29703,19 @@ } } }, + "SetUserCredentialsRequest": { + "type": "object", + "description": "Request to set user credentials (e.g., personal access token)", + "required": [ + "accessToken" + ], + "properties": { + "accessToken": { + "type": "string", + "description": "The access token or API key to store" + } + } + }, "SetUserRatingRequest": { "type": "object", "description": "Request to create or update a user's rating for a series", @@ -29166,50 +30046,186 @@ "string", "null" ], - "description": "Optional description", - "example": "Content appropriate for children" + "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" + } + } + }, + "SkippedField": { + "type": "object", + "description": "A field that was skipped during apply", + "required": [ + "field", + "reason" + ], + "properties": { + "field": { + "type": "string", + "description": "Field name" + }, + "reason": { + "type": "string", + "description": "Reason for skipping" + } + } + }, + "SmartBookConfig": { + "type": "object", + "description": "Configuration for smart book naming strategy", + "properties": { + "additionalGenericPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional patterns to consider as \"generic\" titles (beyond defaults)" + } + } + }, + "SyncStatusDto": { + "type": "object", + "description": "Sync status response for a user plugin", + "required": [ + "pluginId", + "pluginName", + "connected", + "healthStatus", + "failureCount", + "enabled" + ], + "properties": { + "conflicts": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of entries with conflicts on both sides (only with `?live=true`)", + "minimum": 0 + }, + "connected": { + "type": "boolean", + "description": "Whether the plugin is connected and ready to sync" + }, + "enabled": { + "type": "boolean", + "description": "Whether the plugin is currently enabled" + }, + "externalCount": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of entries tracked on the external service (only with `?live=true`)", + "minimum": 0 + }, + "failureCount": { + "type": "integer", + "format": "int32", + "description": "Number of consecutive failures" + }, + "healthStatus": { + "type": "string", + "description": "Health status" + }, + "lastFailureAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last failure timestamp" + }, + "lastSuccessAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last successful operation timestamp" + }, + "lastSyncAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last successful sync timestamp" + }, + "liveError": { + "type": [ + "string", + "null" + ], + "description": "Error message if `?live=true` was requested but the plugin could not be queried" }, - "id": { + "pendingPull": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of external entries that need to be pulled (only with `?live=true`)", + "minimum": 0 + }, + "pendingPush": { + "type": [ + "integer", + "null" + ], + "format": "int32", + "description": "Number of local entries that need to be pushed (only with `?live=true`)", + "minimum": 0 + }, + "pluginId": { "type": "string", "format": "uuid", - "description": "Unique sharing tag identifier", - "example": "550e8400-e29b-41d4-a716-446655440000" + "description": "Plugin ID" }, - "name": { + "pluginName": { "type": "string", - "description": "Display name of the sharing tag", - "example": "Kids Content" + "description": "Plugin name" } } }, - "SkippedField": { + "SyncStatusQuery": { "type": "object", - "description": "A field that was skipped during apply", - "required": [ - "field", - "reason" - ], + "description": "Query parameters for sync status endpoint", "properties": { - "field": { - "type": "string", - "description": "Field name" - }, - "reason": { - "type": "string", - "description": "Reason for skipping" + "live": { + "type": "boolean", + "description": "If true, spawn the plugin process and query live sync state\n(external count, pending push/pull, conflicts).\nDefault: false (returns database-stored metadata only)." } } }, - "SmartBookConfig": { + "SyncTriggerResponse": { "type": "object", - "description": "Configuration for smart book naming strategy", + "description": "Response from triggering a sync operation", + "required": [ + "taskId", + "message" + ], "properties": { - "additionalGenericPatterns": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Additional patterns to consider as \"generic\" titles (beyond defaults)" + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "taskId": { + "type": "string", + "format": "uuid", + "description": "Task ID for tracking the sync operation" } } }, @@ -30215,6 +31231,71 @@ ] } } + }, + { + "type": "object", + "description": "Clean up expired plugin storage data across all user plugins", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "cleanup_plugin_data" + ] + } + } + }, + { + "type": "object", + "description": "Sync user plugin data with external service", + "required": [ + "pluginId", + "userId", + "type" + ], + "properties": { + "pluginId": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "user_plugin_sync" + ] + }, + "userId": { + "type": "string", + "format": "uuid" + } + } + }, + { + "type": "object", + "description": "Refresh recommendations from a user plugin", + "required": [ + "pluginId", + "userId", + "type" + ], + "properties": { + "pluginId": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "user_plugin_recommendations" + ] + }, + "userId": { + "type": "string", + "format": "uuid" + } + } } ], "description": "Task types supported by the distributed task queue" @@ -31296,6 +32377,18 @@ } } }, + "UpdateUserPluginConfigRequest": { + "type": "object", + "description": "Request to update user plugin configuration", + "required": [ + "config" + ], + "properties": { + "config": { + "description": "Configuration overrides for this plugin" + } + } + }, "UpdateUserRequest": { "type": "object", "description": "Update user request", @@ -31552,6 +32645,181 @@ } } }, + "UserPluginCapabilitiesDto": { + "type": "object", + "description": "Plugin capabilities for display (user plugin context)", + "required": [ + "readSync", + "userRecommendationProvider" + ], + "properties": { + "readSync": { + "type": "boolean", + "description": "Can sync reading progress" + }, + "userRecommendationProvider": { + "type": "boolean", + "description": "Can provide recommendations" + } + } + }, + "UserPluginDto": { + "type": "object", + "description": "User plugin instance status", + "required": [ + "id", + "pluginId", + "pluginName", + "pluginDisplayName", + "pluginType", + "enabled", + "connected", + "healthStatus", + "requiresOauth", + "oauthConfigured", + "config", + "capabilities", + "createdAt" + ], + "properties": { + "capabilities": { + "$ref": "#/components/schemas/UserPluginCapabilitiesDto", + "description": "Plugin capabilities (derived from manifest)" + }, + "config": { + "description": "Per-user configuration" + }, + "connected": { + "type": "boolean", + "description": "Whether the plugin is connected (has valid credentials/OAuth)" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Created timestamp" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "User-facing description of the plugin" + }, + "enabled": { + "type": "boolean", + "description": "Whether the user has enabled this plugin" + }, + "externalAvatarUrl": { + "type": [ + "string", + "null" + ], + "description": "External service avatar URL" + }, + "externalUsername": { + "type": [ + "string", + "null" + ], + "description": "External service username (if connected via OAuth)" + }, + "healthStatus": { + "type": "string", + "description": "Health status of this user's plugin instance" + }, + "id": { + "type": "string", + "format": "uuid", + "description": "User plugin instance ID" + }, + "lastSuccessAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last successful operation timestamp" + }, + "lastSyncAt": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Last sync timestamp" + }, + "lastSyncResult": { + "description": "Last sync result summary (stored in user_plugin_data)" + }, + "oauthConfigured": { + "type": "boolean", + "description": "Whether the admin has configured OAuth credentials (client_id set)" + }, + "pluginDisplayName": { + "type": "string", + "description": "Plugin display name for UI" + }, + "pluginId": { + "type": "string", + "format": "uuid", + "description": "Plugin definition ID" + }, + "pluginName": { + "type": "string", + "description": "Plugin display name" + }, + "pluginType": { + "type": "string", + "description": "Plugin type: \"system\" or \"user\"" + }, + "requiresOauth": { + "type": "boolean", + "description": "Whether this plugin requires OAuth authentication" + }, + "userConfigSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ConfigSchemaDto", + "description": "User-facing configuration schema (from plugin manifest)" + } + ] + }, + "userSetupInstructions": { + "type": [ + "string", + "null" + ], + "description": "User-facing setup instructions for the plugin" + } + } + }, + "UserPluginsListResponse": { + "type": "object", + "description": "User plugins list response", + "required": [ + "enabled", + "available" + ], + "properties": { + "available": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailablePluginDto" + }, + "description": "Plugins available for the user to enable" + }, + "enabled": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserPluginDto" + }, + "description": "Plugins the user has enabled" + } + } + }, "UserPreferenceDto": { "type": "object", "description": "A single user preference", @@ -31940,6 +33208,14 @@ "name": "Plugin Actions", "description": "Plugin action discovery and execution for metadata fetching" }, + { + "name": "User Plugins", + "description": "User-facing plugin management, OAuth, and configuration" + }, + { + "name": "Recommendations", + "description": "Personalized recommendation endpoints" + }, { "name": "Metrics", "description": "Application metrics and statistics" @@ -32010,6 +33286,8 @@ "tags": [ "Users", "User Preferences", + "User Plugins", + "Recommendations", "Reading Progress" ] }, diff --git a/web/package-lock.json b/web/package-lock.json index 0d375f4d..27419774 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1133,6 +1133,60 @@ "react": "^18.x || ^19.x" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.41.2", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.2.tgz", @@ -2429,6 +2483,15 @@ "node": ">=10.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2475,6 +2538,49 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2675,18 +2781,21 @@ } }, "node_modules/canvas": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz", - "integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.3" + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" }, "engines": { - "node": "^18.12.0 || >= 20.9.0" + "node": ">=6" } }, "node_modules/ccount": { @@ -2791,11 +2900,16 @@ } }, "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, "license": "ISC", - "optional": true + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } }, "node_modules/ci-info": { "version": "3.9.0", @@ -2885,6 +2999,18 @@ "dev": true, "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", @@ -2914,6 +3040,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -3103,19 +3247,18 @@ } }, "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "mimic-response": "^3.1.0" + "mimic-response": "^2.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/deep-eql": { @@ -3147,6 +3290,15 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3640,6 +3792,45 @@ "license": "MIT", "optional": true }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3664,6 +3855,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3727,6 +3951,58 @@ "license": "MIT", "optional": true }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "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==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -3848,6 +4124,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4135,6 +4420,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4673,6 +4972,36 @@ "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-event-props": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", @@ -5593,13 +5922,15 @@ } }, "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5637,6 +5968,64 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -5764,6 +6153,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5822,6 +6220,94 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -5962,6 +6448,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -6015,6 +6513,21 @@ "path2d": "^0.2.1" } }, + "node_modules/pdfjs-dist/node_modules/canvas": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz", + "integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6227,6 +6740,61 @@ "node": ">=10" } }, + "node_modules/prebuild-install/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6757,6 +7325,25 @@ "dev": true, "license": "MIT" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -6875,6 +7462,15 @@ "seroval": "^1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -6929,27 +7525,15 @@ "optional": true }, "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "decompress-response": "^6.0.0", + "decompress-response": "^4.2.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } @@ -7216,6 +7800,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -7229,6 +7834,13 @@ "tar-stream": "^2.1.4" } }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -9107,6 +9719,18 @@ "node": ">=8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -9184,6 +9808,15 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/yaml-ast-parser": { "version": "0.0.43", "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", diff --git a/web/src/App.tsx b/web/src/App.tsx index e8c4fe78..312cad44 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -19,6 +19,7 @@ import { LibraryPage } from "@/pages/Library"; import { Login } from "@/pages/Login"; import { OidcComplete } from "@/pages/OidcComplete"; import { Reader } from "@/pages/Reader"; +import { Recommendations } from "@/pages/Recommendations"; import { Register } from "@/pages/Register"; import { SearchResults } from "@/pages/SearchResults"; import { SeriesDetail } from "@/pages/SeriesDetail"; @@ -27,6 +28,7 @@ import { BooksInErrorSettings, CleanupSettings, DuplicatesSettings, + IntegrationsSettings, MetricsSettings, PdfCacheSettings, PluginsSettings, @@ -152,6 +154,17 @@ function App() { } /> + + + + + + } + /> + {/* Settings routes */} + + + + + + } + /> + => { + const response = await api.get( + "/user/recommendations", + ); + return response.data; + }, + + /** + * Refresh recommendations (clears cache and regenerates) + */ + refresh: async (): Promise => { + const response = await api.post( + "/user/recommendations/refresh", + ); + return response.data; + }, + + /** + * Dismiss a recommendation (user not interested) + */ + dismiss: async ( + externalId: string, + reason?: string, + ): Promise => { + const response = await api.post( + `/user/recommendations/${encodeURIComponent(externalId)}/dismiss`, + { reason } satisfies DismissRecommendationRequest, + ); + return response.data; + }, +}; diff --git a/web/src/api/userPlugins.ts b/web/src/api/userPlugins.ts new file mode 100644 index 00000000..7096c886 --- /dev/null +++ b/web/src/api/userPlugins.ts @@ -0,0 +1,145 @@ +import type { TaskResponse } from "@/types"; +import type { components } from "@/types/api.generated"; +import { api } from "./client"; + +// ============================================================================= +// Types (from generated OpenAPI types) +// ============================================================================= + +export type UserPluginDto = components["schemas"]["UserPluginDto"]; +export type AvailablePluginDto = components["schemas"]["AvailablePluginDto"]; +export type UserPluginCapabilitiesDto = + components["schemas"]["UserPluginCapabilitiesDto"]; +export type UserPluginsListResponse = + components["schemas"]["UserPluginsListResponse"]; +export type OAuthStartResponse = components["schemas"]["OAuthStartResponse"]; +export type UpdateUserPluginConfigRequest = + components["schemas"]["UpdateUserPluginConfigRequest"]; +export type SyncTriggerResponse = components["schemas"]["SyncTriggerResponse"]; +export type SyncStatusDto = components["schemas"]["SyncStatusDto"]; +export type ConfigSchemaDto = components["schemas"]["ConfigSchemaDto"]; + +// ============================================================================= +// API Client +// ============================================================================= + +export const userPluginsApi = { + /** + * List the current user's plugins (enabled and available) + */ + list: async (): Promise => { + const response = await api.get("/user/plugins"); + return response.data; + }, + + /** + * Get a single user plugin instance + */ + get: async (pluginId: string): Promise => { + const response = await api.get(`/user/plugins/${pluginId}`); + return response.data; + }, + + /** + * Enable a plugin for the current user + */ + enable: async (pluginId: string): Promise => { + const response = await api.post( + `/user/plugins/${pluginId}/enable`, + ); + return response.data; + }, + + /** + * Disable a plugin for the current user + */ + disable: async (pluginId: string): Promise<{ success: boolean }> => { + const response = await api.post<{ success: boolean }>( + `/user/plugins/${pluginId}/disable`, + ); + return response.data; + }, + + /** + * Update user-specific plugin configuration + */ + updateConfig: async ( + pluginId: string, + config: Record, + ): Promise => { + const response = await api.patch( + `/user/plugins/${pluginId}/config`, + { config } satisfies UpdateUserPluginConfigRequest, + ); + return response.data; + }, + + /** + * Disconnect a plugin (remove all data and credentials) + */ + disconnect: async (pluginId: string): Promise<{ success: boolean }> => { + const response = await api.delete<{ success: boolean }>( + `/user/plugins/${pluginId}`, + ); + return response.data; + }, + + /** + * Start OAuth flow for a plugin + * Returns a redirect URL to open in a popup window + */ + startOAuth: async (pluginId: string): Promise => { + const response = await api.post( + `/user/plugins/${pluginId}/oauth/start`, + ); + return response.data; + }, + + /** + * Trigger a sync operation for a plugin + */ + triggerSync: async (pluginId: string): Promise => { + const response = await api.post( + `/user/plugins/${pluginId}/sync`, + ); + return response.data; + }, + + /** + * Get sync status for a plugin + * Pass live=true to query the plugin process for real-time counts (more expensive) + */ + getSyncStatus: async ( + pluginId: string, + live = false, + ): Promise => { + const response = await api.get( + `/user/plugins/${pluginId}/sync/status`, + { params: live ? { live: true } : undefined }, + ); + return response.data; + }, + + /** + * Set user credentials (personal access token) + * Used when OAuth is not configured by admin + */ + setCredentials: async ( + pluginId: string, + accessToken: string, + ): Promise => { + const response = await api.post( + `/user/plugins/${pluginId}/credentials`, + { accessToken }, + ); + return response.data; + }, + + /** + * Get a task by ID (for polling sync task completion) + */ + getTask: async (taskId: string): Promise => { + const response = await api.get(`/tasks/${taskId}`); + return response.data; + }, +}; diff --git a/web/src/components/books/BookMetadataEditModal.tsx b/web/src/components/books/BookMetadataEditModal.tsx index 1764f0c4..c9ca7b7f 100644 --- a/web/src/components/books/BookMetadataEditModal.tsx +++ b/web/src/components/books/BookMetadataEditModal.tsx @@ -48,9 +48,7 @@ import { LockableTextarea, } from "@/components/forms/lockable"; import { extractSourceFromUrl } from "@/components/series/ExternalLinks"; -import type { components } from "@/types"; - -type BookTypeDto = components["schemas"]["BookTypeDto"]; +import type { BookTypeDto } from "@/types"; const BOOK_TYPE_OPTIONS = [ { value: "comic", label: "Comic" }, diff --git a/web/src/components/forms/AddLibraryModal.test.tsx b/web/src/components/forms/AddLibraryModal.test.tsx index f521eead..ed9beaff 100644 --- a/web/src/components/forms/AddLibraryModal.test.tsx +++ b/web/src/components/forms/AddLibraryModal.test.tsx @@ -1,5 +1,5 @@ -import { cleanup, screen, waitFor, within } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { screen, waitFor, within } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { filesystemApi } from "@/api/filesystem"; import { librariesApi } from "@/api/libraries"; import { renderWithProviders, userEvent } from "@/test/utils"; @@ -8,6 +8,11 @@ import { LibraryModal } from "./LibraryModal"; vi.mock("@/api/filesystem"); vi.mock("@/api/libraries"); +vi.mock("@mantine/notifications", () => ({ + notifications: { + show: vi.fn(), + }, +})); // Helper to create a complete Library mock with all required fields const createMockLibrary = (overrides?: Partial): Library => ({ @@ -66,11 +71,18 @@ const mockBrowseResponse: BrowseResponse = { describe("LibraryModal (Add Mode)", () => { const mockOnClose = vi.fn(); + const originalScrollIntoView = Element.prototype.scrollIntoView; beforeEach(() => { vi.clearAllMocks(); vi.mocked(filesystemApi.getDrives).mockResolvedValue(mockDrives); vi.mocked(filesystemApi.browse).mockResolvedValue(mockBrowseResponse); + // Mock scrollIntoView for Mantine Combobox + Element.prototype.scrollIntoView = vi.fn(); + }); + + afterEach(() => { + Element.prototype.scrollIntoView = originalScrollIntoView; }); it("should not render when closed", () => { @@ -831,7 +843,9 @@ describe("LibraryModal (Add Mode)", () => { it("should reset to all formats when modal closes", async () => { const user = userEvent.setup(); - renderWithProviders(); + const { unmount } = renderWithProviders( + , + ); // Wait for modal const modal = await screen.findByRole("dialog", {}, { timeout: 3000 }); @@ -874,17 +888,18 @@ describe("LibraryModal (Add Mode)", () => { expect(mockOnClose).toHaveBeenCalled(); - // Clean up the first render before re-rendering - cleanup(); + // Unmount the first render before re-rendering + unmount(); // Reopen modal - formats should be reset to all + const user2 = userEvent.setup(); renderWithProviders(); const newModal = await screen.findByRole("dialog", {}, { timeout: 3000 }); const newModalContent = within(newModal); const newFormatsInput = newModalContent.getByLabelText("Allowed Formats"); - await user.click(newFormatsInput); + await user2.click(newFormatsInput); // All formats should be available again (may appear multiple times in MultiSelect) // Use getAllByText to handle multiple instances - this is expected behavior diff --git a/web/src/components/forms/PluginConfigModal.test.tsx b/web/src/components/forms/PluginConfigModal.test.tsx new file mode 100644 index 00000000..0b3c9aad --- /dev/null +++ b/web/src/components/forms/PluginConfigModal.test.tsx @@ -0,0 +1,300 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { PluginDto } from "@/api/plugins"; +import { renderWithProviders, screen, waitFor } from "@/test/utils"; +import { PluginConfigModal } from "./PluginConfigModal"; + +// Mock the APIs +vi.mock("@/api/plugins", async () => { + const actual = await vi.importActual("@/api/plugins"); + return { + ...actual, + pluginsApi: { + update: vi.fn(), + }, + }; +}); + +vi.mock("@mantine/notifications", () => ({ + notifications: { + show: vi.fn(), + }, +})); + +vi.mock("@/utils/templateUtils", () => ({ + SAMPLE_SERIES_CONTEXT: { + seriesId: "test-id", + bookCount: 10, + metadata: { + title: "One Piece (Digital)", + titleSort: "One Piece", + year: 1999, + publisher: "Shueisha", + language: "en", + status: "ongoing", + ageRating: null, + genres: ["Action", "Adventure"], + tags: ["pirates"], + }, + }, +})); + +// Minimal mock for sub-editors to avoid deep dependency issues +vi.mock("./PreprocessingRulesEditor", () => ({ + PreprocessingRulesEditor: () => ( +

+ ), +})); + +vi.mock("./ConditionsEditor", () => ({ + ConditionsEditor: () =>
, +})); + +function createMockPlugin(overrides: Partial = {}): PluginDto { + return { + id: "plugin-1", + name: "test-plugin", + displayName: "Test Plugin", + description: "A test plugin", + pluginType: "system", + command: "node", + args: ["index.js"], + workingDirectory: null, + env: {}, + permissions: ["metadata:read"], + scopes: ["series:detail"], + libraryIds: [], + credentialDelivery: "env", + hasCredentials: false, + config: {}, + searchPreprocessingRules: null, + autoMatchConditions: null, + metadataTargets: null, + searchQueryTemplate: null, + useExistingExternalId: false, + enabled: true, + healthStatus: "healthy", + failureCount: 0, + lastFailureAt: null, + lastSuccessAt: null, + disabledReason: null, + manifest: null, + rateLimitRequestsPerMinute: 60, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + ...overrides, + } as PluginDto; +} + +const mockLibraries = [ + { id: "lib-1", name: "Comics" }, + { id: "lib-2", name: "Manga" }, +]; + +describe("PluginConfigModal", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders with Permissions tab for any plugin", () => { + const plugin = createMockPlugin(); + renderWithProviders( + , + ); + + expect( + screen.getByRole("tab", { name: /Permissions/ }), + ).toBeInTheDocument(); + }); + + it("shows search tabs for metadata provider plugins", () => { + const plugin = createMockPlugin({ + manifest: { + name: "metadata-plugin", + displayName: "Metadata Plugin", + version: "1.0.0", + protocolVersion: "1.0", + description: "A metadata plugin", + capabilities: { + metadataProvider: ["series"], + userReadSync: false, + }, + contentTypes: ["series"], + requiredCredentials: [], + scopes: ["series:detail"], + }, + }); + + renderWithProviders( + , + ); + + expect( + screen.getByRole("tab", { name: /Permissions/ }), + ).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /Template/ })).toBeInTheDocument(); + expect( + screen.getByRole("tab", { name: /Preprocessing/ }), + ).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /Conditions/ })).toBeInTheDocument(); + }); + + it("hides search tabs for sync-only plugins", () => { + const plugin = createMockPlugin({ + manifest: { + name: "sync-plugin", + displayName: "Sync Plugin", + version: "1.0.0", + protocolVersion: "1.0", + description: "A sync plugin", + capabilities: { + metadataProvider: [], + userReadSync: true, + }, + contentTypes: [], + requiredCredentials: [], + scopes: [], + }, + }); + + renderWithProviders( + , + ); + + expect( + screen.getByRole("tab", { name: /Permissions/ }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: /Template/ }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: /Preprocessing/ }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: /Conditions/ }), + ).not.toBeInTheDocument(); + }); + + it("hides search tabs for plugins with no manifest", () => { + const plugin = createMockPlugin({ manifest: null }); + + renderWithProviders( + , + ); + + expect( + screen.getByRole("tab", { name: /Permissions/ }), + ).toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: /Template/ }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: /Preprocessing/ }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: /Conditions/ }), + ).not.toBeInTheDocument(); + }); + + it("shows no-manifest warning when plugin has no manifest", async () => { + const plugin = createMockPlugin({ manifest: null }); + + renderWithProviders( + , + ); + + await waitFor(() => { + expect( + screen.getByText(/This plugin has not been tested yet/), + ).toBeInTheDocument(); + }); + }); + + it("does not show no-manifest warning when plugin has a manifest", () => { + const plugin = createMockPlugin({ + manifest: { + name: "metadata-plugin", + displayName: "Metadata Plugin", + version: "1.0.0", + protocolVersion: "1.0", + description: "A metadata plugin", + capabilities: { + metadataProvider: ["series"], + userReadSync: false, + }, + contentTypes: ["series"], + requiredCredentials: [], + scopes: ["series:detail"], + }, + }); + + renderWithProviders( + , + ); + + expect( + screen.queryByText(/This plugin has not been tested yet/), + ).not.toBeInTheDocument(); + }); + + it("shows modal title with plugin display name", () => { + const plugin = createMockPlugin({ displayName: "MangaBaka" }); + + renderWithProviders( + , + ); + + expect(screen.getByText("Configure: MangaBaka")).toBeInTheDocument(); + }); + + it("does not render when opened is false", () => { + const plugin = createMockPlugin(); + + renderWithProviders( + , + ); + + expect( + screen.queryByText("Configure: Test Plugin"), + ).not.toBeInTheDocument(); + }); +}); diff --git a/web/src/components/forms/PluginConfigModal.tsx b/web/src/components/forms/PluginConfigModal.tsx new file mode 100644 index 00000000..b8e4b2b6 --- /dev/null +++ b/web/src/components/forms/PluginConfigModal.tsx @@ -0,0 +1,290 @@ +import { Button, Group, Modal, Stack, Tabs } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { notifications } from "@mantine/notifications"; +import { + IconCode, + IconKey, + IconSearch, + IconSettings, + IconShield, +} from "@tabler/icons-react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import type { PluginDto } from "@/api/plugins"; +import { pluginsApi } from "@/api/plugins"; +import type { AutoMatchConditions } from "./ConditionsEditor"; +import type { PreprocessingRule } from "./PreprocessingRulesEditor"; +import { + ConditionsTab, + isMetadataProvider, + isOAuthPlugin, + type MetadataTarget, + OAuthTab, + PermissionsTab, + type PluginConfigFormValues, + PreprocessingTab, + TemplateTab, +} from "./plugin-config"; + +// ============================================================================= +// Inner content component (keyed by plugin.id for clean remounts) +// ============================================================================= + +function PluginConfigContent({ + plugin, + onClose, + libraries, +}: { + plugin: PluginDto; + onClose: () => void; + libraries: { id: string; name: string }[]; +}) { + const queryClient = useQueryClient(); + const isMeta = isMetadataProvider(plugin); + const isOAuth = isOAuthPlugin(plugin); + const [activeTab, setActiveTab] = useState("permissions"); + + // Parse initial preprocessing rules from plugin + const initialPreprocessingRules: PreprocessingRule[] = + plugin.searchPreprocessingRules && + Array.isArray(plugin.searchPreprocessingRules) + ? (plugin.searchPreprocessingRules as PreprocessingRule[]) + : []; + + // Parse initial auto-match conditions from plugin + const initialAutoMatchConditions: AutoMatchConditions | null = + plugin.autoMatchConditions && typeof plugin.autoMatchConditions === "object" + ? (plugin.autoMatchConditions as AutoMatchConditions) + : null; + + // State for the complex editors + const [preprocessingRules, setPreprocessingRules] = useState< + PreprocessingRule[] + >(initialPreprocessingRules); + const [autoMatchConditions, setAutoMatchConditions] = + useState(initialAutoMatchConditions); + const [testTitle, setTestTitle] = useState(""); + + // Determine which targets the plugin's manifest supports + const pluginCapabilities = + plugin.manifest?.capabilities?.metadataProvider ?? []; + const canSeries = pluginCapabilities.includes("series"); + const canBook = pluginCapabilities.includes("book"); + + // Parse initial metadata targets from plugin + const initialMetadataTargets: MetadataTarget[] = plugin.metadataTargets + ? (plugin.metadataTargets.filter( + (t): t is MetadataTarget => t === "series" || t === "book", + ) as MetadataTarget[]) + : (["series", "book"].filter((t) => + t === "series" ? canSeries : canBook, + ) as MetadataTarget[]); + + // Extract OAuth config from plugin.config JSON + const pluginConfig = plugin.config as Record | null; + const initialOAuthClientId = + typeof pluginConfig?.oauth_client_id === "string" + ? pluginConfig.oauth_client_id + : ""; + const initialOAuthClientSecret = + typeof pluginConfig?.oauth_client_secret === "string" + ? pluginConfig.oauth_client_secret + : ""; + + // Form for all fields + const form = useForm({ + initialValues: { + permissions: plugin.permissions, + scopes: plugin.scopes, + allLibraries: plugin.libraryIds.length === 0, + libraryIds: plugin.libraryIds, + searchQueryTemplate: plugin.searchQueryTemplate ?? "", + useExistingExternalId: plugin.useExistingExternalId ?? true, + metadataTargets: initialMetadataTargets, + oauthClientId: initialOAuthClientId, + oauthClientSecret: initialOAuthClientSecret, + }, + }); + + const updateMutation = useMutation({ + mutationFn: async () => { + const payload: Record = { + permissions: form.values.permissions, + scopes: form.values.scopes, + libraryIds: form.values.allLibraries ? [] : form.values.libraryIds, + }; + + if (isMeta) { + payload.searchQueryTemplate = + form.values.searchQueryTemplate.trim() || null; + payload.searchPreprocessingRules = preprocessingRules; + payload.autoMatchConditions = autoMatchConditions; + payload.useExistingExternalId = form.values.useExistingExternalId; + payload.metadataTargets = form.values.metadataTargets; + } + + if (isOAuth) { + const existingConfig = (plugin.config as Record) ?? {}; + const config: Record = { ...existingConfig }; + if (form.values.oauthClientId.trim()) { + config.oauth_client_id = form.values.oauthClientId.trim(); + } else { + delete config.oauth_client_id; + } + if (form.values.oauthClientSecret.trim()) { + config.oauth_client_secret = form.values.oauthClientSecret.trim(); + } else { + delete config.oauth_client_secret; + } + payload.config = config; + } + + return pluginsApi.update(plugin.id, payload); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["plugins"] }); + queryClient.invalidateQueries({ queryKey: ["plugin-actions"] }); + notifications.show({ + title: "Success", + message: "Plugin configuration updated successfully", + color: "green", + }); + onClose(); + }, + onError: (error: Error) => { + notifications.show({ + title: "Error", + message: error.message || "Failed to update plugin configuration", + color: "red", + }); + }, + }); + + return ( + <> + + + }> + Permissions + + {isOAuth && ( + }> + OAuth + + )} + {isMeta && ( + <> + }> + Template + + } + > + Preprocessing + + } + > + Conditions + + + )} + + + + + + + + {isOAuth && ( + + + + )} + + {isMeta && ( + <> + + + + + + + + + + + + + )} + + + + + + + + + ); +} + +// ============================================================================= +// Exported modal component +// ============================================================================= + +interface PluginConfigModalProps { + plugin: PluginDto; + opened: boolean; + onClose: () => void; + libraries: { id: string; name: string }[]; +} + +/** + * Modal for configuring plugin permissions, scopes, library access, + * and (for metadata providers) search settings. + * + * Shows capability-aware tabs based on the plugin's manifest. + */ +export function PluginConfigModal({ + plugin, + opened, + onClose, + libraries, +}: PluginConfigModalProps) { + return ( + + {/* Key forces remount when plugin changes, resetting all form state */} + + + ); +} diff --git a/web/src/components/forms/SearchConfigModal.tsx b/web/src/components/forms/SearchConfigModal.tsx deleted file mode 100644 index ed5605ba..00000000 --- a/web/src/components/forms/SearchConfigModal.tsx +++ /dev/null @@ -1,492 +0,0 @@ -import { - Alert, - Badge, - Button, - Card, - Chip, - Code, - Group, - Modal, - Paper, - Stack, - Switch, - Tabs, - Text, - Textarea, - Tooltip, -} from "@mantine/core"; -import { useForm } from "@mantine/form"; -import { notifications } from "@mantine/notifications"; -import { - IconCode, - IconInfoCircle, - IconSearch, - IconSettings, -} from "@tabler/icons-react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useMemo, useState } from "react"; -import type { PluginDto } from "@/api/plugins"; -import { pluginsApi } from "@/api/plugins"; -import { SAMPLE_SERIES_CONTEXT } from "@/utils/templateUtils"; -import { type AutoMatchConditions, ConditionsEditor } from "./ConditionsEditor"; -import { - type PreprocessingRule, - PreprocessingRulesEditor, -} from "./PreprocessingRulesEditor"; - -/** - * Available template helpers. - */ -const TEMPLATE_HELPERS = [ - { - name: "clean", - example: "{{clean metadata.title}}", - description: "Remove noise (Digital, year, etc.)", - }, - { - name: "truncate", - example: "{{truncate metadata.title 50}}", - description: "Limit to N characters", - }, - { - name: "first_word", - example: "{{first_word metadata.title}}", - description: "First word only", - }, - { - name: "lowercase", - example: "{{lowercase metadata.title}}", - description: "Convert to lowercase", - }, -] as const; - -/** - * Render a preview of the template with sample data. - */ -function renderTemplatePreview(template: string): string { - if (!template.trim()) return "(default: series title)"; - - let preview = template; - const ctx = SAMPLE_SERIES_CONTEXT; - const meta = ctx.metadata; - - // Replace top-level field references - preview = preview.replace(/\{\{bookCount\}\}/g, String(ctx.bookCount ?? 0)); - preview = preview.replace(/\{\{seriesId\}\}/g, ctx.seriesId ?? ""); - - // Replace metadata field references - preview = preview.replace(/\{\{metadata\.title\}\}/g, meta?.title ?? ""); - preview = preview.replace( - /\{\{metadata\.titleSort\}\}/g, - meta?.titleSort ?? "", - ); - preview = preview.replace( - /\{\{metadata\.year\}\}/g, - String(meta?.year ?? ""), - ); - preview = preview.replace( - /\{\{metadata\.publisher\}\}/g, - meta?.publisher ?? "", - ); - preview = preview.replace( - /\{\{metadata\.language\}\}/g, - meta?.language ?? "", - ); - preview = preview.replace(/\{\{metadata\.status\}\}/g, meta?.status ?? ""); - preview = preview.replace( - /\{\{metadata\.ageRating\}\}/g, - String(meta?.ageRating ?? ""), - ); - preview = preview.replace( - /\{\{metadata\.genres\}\}/g, - meta?.genres?.join(", ") ?? "", - ); - preview = preview.replace( - /\{\{metadata\.tags\}\}/g, - meta?.tags?.join(", ") ?? "", - ); - - // Simplify helper calls for preview - preview = preview.replace(/\{\{clean metadata\.title\}\}/g, "One Piece"); - preview = preview.replace( - /\{\{truncate metadata\.title \d+\}\}/g, - "One Piece (D...", - ); - preview = preview.replace(/\{\{first_word metadata\.title\}\}/g, "One"); - preview = preview.replace( - /\{\{lowercase metadata\.title\}\}/g, - "one piece (digital)", - ); - - // Handle conditionals (simplified) - preview = preview.replace(/\{\{#if [\w.]+\}\}(.*?)\{\{\/if\}\}/g, "$1"); - preview = preview.replace(/\{\{#unless [\w.]+\}\}(.*?)\{\{\/unless\}\}/g, ""); - - return preview || "(empty)"; -} - -interface SearchConfigModalProps { - /** The plugin to configure */ - plugin: PluginDto; - /** Whether the modal is open */ - opened: boolean; - /** Callback when modal is closed */ - onClose: () => void; -} - -/** Valid metadata target values */ -type MetadataTarget = "series" | "book"; - -interface SearchConfigFormValues { - searchQueryTemplate: string; - useExistingExternalId: boolean; - metadataTargets: MetadataTarget[]; -} - -/** - * Inner content component that handles the form state. - * Separated to ensure state resets when plugin changes via key prop. - */ -function SearchConfigContent({ - plugin, - onClose, -}: { - plugin: PluginDto; - onClose: () => void; -}) { - const queryClient = useQueryClient(); - const [activeTab, setActiveTab] = useState("template"); - - // Parse initial preprocessing rules from plugin - const initialPreprocessingRules: PreprocessingRule[] = - plugin.searchPreprocessingRules && - Array.isArray(plugin.searchPreprocessingRules) - ? (plugin.searchPreprocessingRules as PreprocessingRule[]) - : []; - - // Parse initial auto-match conditions from plugin - const initialAutoMatchConditions: AutoMatchConditions | null = - plugin.autoMatchConditions && typeof plugin.autoMatchConditions === "object" - ? (plugin.autoMatchConditions as AutoMatchConditions) - : null; - - // State for the complex editors - const [preprocessingRules, setPreprocessingRules] = useState< - PreprocessingRule[] - >(initialPreprocessingRules); - const [autoMatchConditions, setAutoMatchConditions] = - useState(initialAutoMatchConditions); - const [testTitle, setTestTitle] = useState(""); - - // Determine which targets the plugin's manifest supports - const pluginCapabilities = - plugin.manifest?.capabilities?.metadataProvider ?? []; - const canSeries = pluginCapabilities.includes("series"); - const canBook = pluginCapabilities.includes("book"); - - // Parse initial metadata targets from plugin - // null/undefined means "auto" (both), otherwise use the explicit array - const initialMetadataTargets: MetadataTarget[] = plugin.metadataTargets - ? (plugin.metadataTargets.filter( - (t): t is MetadataTarget => t === "series" || t === "book", - ) as MetadataTarget[]) - : (["series", "book"].filter((t) => - t === "series" ? canSeries : canBook, - ) as MetadataTarget[]); - - // Form for simple fields - const form = useForm({ - initialValues: { - searchQueryTemplate: plugin.searchQueryTemplate ?? "", - useExistingExternalId: plugin.useExistingExternalId ?? true, - metadataTargets: initialMetadataTargets, - }, - }); - - // Live preview of the template - const templatePreview = useMemo( - () => renderTemplatePreview(form.values.searchQueryTemplate), - [form.values.searchQueryTemplate], - ); - - const updateMutation = useMutation({ - mutationFn: async () => { - return pluginsApi.update(plugin.id, { - searchQueryTemplate: form.values.searchQueryTemplate.trim() || null, - // Always send the value to allow clearing - empty array or null clears the rules - searchPreprocessingRules: preprocessingRules, - autoMatchConditions: autoMatchConditions, - useExistingExternalId: form.values.useExistingExternalId, - metadataTargets: form.values.metadataTargets, - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["plugins"] }); - notifications.show({ - title: "Success", - message: "Search configuration updated successfully", - color: "green", - }); - onClose(); - }, - onError: (error: Error) => { - notifications.show({ - title: "Error", - message: error.message || "Failed to update search configuration", - color: "red", - }); - }, - }); - - const handleSubmit = () => { - updateMutation.mutate(); - }; - - return ( - <> - - - }> - Template - - } - > - Preprocessing - - }> - Conditions - - - - - - - } - color="blue" - variant="light" - > - - Customize the search query using Handlebars syntax. The - template has access to series context data shown below. - - - - {/* Template input */} - - - Search Query Template - -