diff --git a/.dockerignore b/.dockerignore index 5e00924..2f211f2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,10 @@ bin obj **/bin **/obj +**/node_modules +**/dist +**/coverage +src/SecureFix.Api/wwwroot *.db *.sqlite *.sqlite3 diff --git a/.env.example b/.env.example index e8c69cb..d487015 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,15 @@ AUTH_MODE=demo # Used only by the demo authentication handler (AUTH_MODE=demo). Never set in Production. SECUREFIX_DEMO_TOKEN=securefix-demo-token +# Dashboard build configuration (public values; never place secrets in VITE_* variables) +VITE_DATA_MODE=live +VITE_AUTH_MODE=demo +# Required for a production Entra dashboard build: +# VITE_ENTRA_CLIENT_ID= +# VITE_ENTRA_TENANT_ID= +# VITE_ENTRA_API_SCOPE=api:///access_as_user +# VITE_ENTRA_REDIRECT_URI=https://securefix.example.com + # Database DATABASE_URL=sqlite:securefix.db DATABASE_PROVIDER=sqlite diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index ca90d28..18c29c3 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -27,6 +27,32 @@ jobs: with: dotnet-version: '10.0.x' + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '22' + cache: npm + cache-dependency-path: src/SecureFix.Web/package-lock.json + + - name: Install dashboard dependencies + working-directory: src/SecureFix.Web + run: npm ci + + - name: Test dashboard + working-directory: src/SecureFix.Web + run: npm test + + - name: Build dashboard + working-directory: src/SecureFix.Web + env: + VITE_DATA_MODE: live + VITE_AUTH_MODE: demo + run: npm run build + + - name: Audit dashboard production dependencies + working-directory: src/SecureFix.Web + run: npm audit --omit=dev --audit-level=high + - name: Restore dependencies run: dotnet restore SecureFix.slnx diff --git a/.gitignore b/.gitignore index f39b1bb..81bdde6 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ logs/ artifacts/ build-output/ publish/ +src/SecureFix.Api/wwwroot/ demo-results-* demo-security-* diff --git a/Dockerfile b/Dockerfile index 89b9b85..2db97ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,35 @@ -# Build stage +# Dashboard build stage +FROM node:22-alpine AS web-build +WORKDIR /src/SecureFix.Web + +COPY ["src/SecureFix.Web/package.json", "src/SecureFix.Web/package-lock.json", "./"] +RUN npm ci +COPY ["src/SecureFix.Web/", "./"] + +ARG VITE_DATA_MODE=live +ARG VITE_AUTH_MODE=demo +ARG VITE_ENTRA_CLIENT_ID= +ARG VITE_ENTRA_TENANT_ID= +ARG VITE_ENTRA_API_SCOPE= +ARG VITE_ENTRA_REDIRECT_URI= +ENV VITE_DATA_MODE=$VITE_DATA_MODE \ + VITE_AUTH_MODE=$VITE_AUTH_MODE \ + VITE_ENTRA_CLIENT_ID=$VITE_ENTRA_CLIENT_ID \ + VITE_ENTRA_TENANT_ID=$VITE_ENTRA_TENANT_ID \ + VITE_ENTRA_API_SCOPE=$VITE_ENTRA_API_SCOPE \ + VITE_ENTRA_REDIRECT_URI=$VITE_ENTRA_REDIRECT_URI +RUN npm run build + +# API build stage FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Copy solution and projects COPY ["SecureFix.slnx", "."] COPY ["src/SecureFix.Core/", "src/SecureFix.Core/"] COPY ["src/SecureFix.Api/", "src/SecureFix.Api/"] COPY ["tests/SecureFix.Tests/", "tests/SecureFix.Tests/"] +COPY --from=web-build /src/SecureFix.Api/wwwroot/ src/SecureFix.Api/wwwroot/ -# Restore and build RUN dotnet restore SecureFix.slnx RUN dotnet build SecureFix.slnx -c Release -o /app/build @@ -26,6 +47,6 @@ RUN (id -u app >/dev/null 2>&1 || useradd -m -u 1000 app) && chown -R app:app /a USER app EXPOSE 5000 -ENV ASPNETCORE_URLS=http://+:5000 +ENV ASPNETCORE_URLS=http://0.0.0.0:5000 ENTRYPOINT ["dotnet", "SecureFix.Api.dll"] diff --git a/README.md b/README.md index 89a3876..2126c6b 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ Dependabot or JSON alert ### How would this be deployed? -Hackathon demo: containerized FastAPI app with SQLite and local/mock providers. +Hackathon demo: a React dashboard and ASP.NET Core API shipped as one container, with SQLite and local/mock providers. -Production path: separately scalable API and worker services on Azure Container Apps or AKS, backed by managed messaging, PostgreSQL, managed identity, secrets management, and centralized observability. +Production path: the integrated dashboard/API container can run on Azure Container Apps or AKS, with workers scaled separately and backed by managed messaging, PostgreSQL, managed identity, secrets management, and centralized observability. ### How would this be secured? @@ -67,8 +67,8 @@ AI timeout or invalid output falls back to a rules-based recommendation and requ - automatic merge - automatic production deployment - autonomous approval -- full enterprise identity integration -- complex front-end UI +- automatic production deployment from the dashboard +- direct merge or release actions ## Repository guide @@ -94,12 +94,23 @@ The intended demo should show: 7. a blocked prompt-injection or unauthorized-action attempt 8. a fallback path when AI is unavailable -## Running the local demo +## Running the dashboard locally -Start the API, then run the authenticated end-to-end validation script in a second terminal: +Build the integrated dashboard, then start the API: ```bash +cd src/SecureFix.Web +npm ci +VITE_DATA_MODE=live VITE_AUTH_MODE=demo npm run build +cd ../.. dotnet run --project src/SecureFix.Api --urls http://127.0.0.1:5000 +``` + +Open `http://127.0.0.1:5000`. The development role switcher uses the existing demo bearer token and headers; it must never be enabled in Production. For frontend hot reload, run `npm run dev` and let Vite proxy API calls to the configured ASP.NET development URL. + +To run the authenticated API validation script in a second terminal: + +```bash bash ./demo-end-to-end.sh ``` @@ -114,8 +125,15 @@ writes evidence to `demo-security-results-*`. bash ./demo-security-controls.sh ``` -## Status +## Dashboard capabilities -This repository is organized around the implementation roadmap and supporting docs for the hackathon MVP. +- operational overview with workflow and severity distributions +- searchable, filterable vulnerability work queue +- workflow detail with risk, approval, remediation, proposal, audit, and governance views +- validated alert ingestion and sample payloads +- role-aware human approval and rejection actions +- demo identity switching for local development and an MSAL/Entra production adapter + +## Status -The preferred delivery approach is a reproducible, production-minded demo that favors security, governance, traceability, and human oversight over UI polish. +The repository provides a reproducible, production-minded demo that combines a governed security workflow with an operator dashboard while preserving security, traceability, and human oversight. diff --git a/docker-compose.yml b/docker-compose.yml index d4d80ea..7088d93 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,27 @@ services: securefix-api: + env_file: + - .env build: context: . dockerfile: Dockerfile + args: + VITE_DATA_MODE: ${VITE_DATA_MODE:-live} + VITE_AUTH_MODE: ${VITE_AUTH_MODE:-demo} + VITE_ENTRA_CLIENT_ID: ${VITE_ENTRA_CLIENT_ID:-} + VITE_ENTRA_TENANT_ID: ${VITE_ENTRA_TENANT_ID:-} + VITE_ENTRA_API_SCOPE: ${VITE_ENTRA_API_SCOPE:-} + VITE_ENTRA_REDIRECT_URI: ${VITE_ENTRA_REDIRECT_URI:-} ports: - "8888:5000" environment: ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Development} ASPNETCORE_URLS: ${ASPNETCORE_URLS:-http://+:5000} + AUTH_MODE: ${AUTH_MODE:-entra} + AzureAd__TenantId: ${AzureAd__TenantId:-} + AzureAd__ClientId: ${AzureAd__ClientId:-} + AzureAd__Audience: ${AzureAd__Audience:-} + SECUREFIX_DEMO_TOKEN: ${SECUREFIX_DEMO_TOKEN:-securefix-demo-token} DATABASE_URL: ${DATABASE_URL:-sqlite:securefix.db} DATABASE_PROVIDER: ${DATABASE_PROVIDER:-sqlite} AI_PROVIDER: ${AI_PROVIDER:-mock} diff --git a/docs/architecture.md b/docs/architecture.md index aa60cf8..4b99f49 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,15 +12,18 @@ SecureFix AI uses a controlled workflow: ## Target shape -- FastAPI API for ingestion and workflow actions -- service layer for business logic +- React and TypeScript dashboard built with Vite and served by the API as static assets +- ASP.NET Core API for ingestion, dashboard queries, and workflow actions +- service layer for business logic and read-optimized dashboard aggregation - repositories for persistence abstraction -- security layer for identity and authorization -- governance layer for approval and reporting -- observability layer for logs, metrics, and traces +- security layer for demo or Microsoft Entra identity and role authorization +- governance layer for approval, audit, and reporting +- observability layer for logs, health, readiness, and process metrics + +The dashboard and API use same-origin requests in the integrated deployment. UI role gating improves usability, but API authorization remains authoritative. Durable dashboard counts come from persisted workflow records; `/metrics` remains Admin-only process telemetry. ## Deployment path -Hackathon demo: containerized app with SQLite and local/mock providers. +Hackathon demo: one container serving the compiled dashboard and API, with SQLite and local/mock providers. Production target: separately scalable containers with managed identity, managed queueing, PostgreSQL, secrets management, and centralized telemetry. diff --git a/docs/runbook.md b/docs/runbook.md index d2bcd87..862ecc1 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -2,6 +2,7 @@ ## Common operations +- build and start the integrated dashboard/API - start the service in Docker - run unit and security tests - submit a sample alert @@ -10,6 +11,20 @@ - inspect audit and governance output - provision or update the Entra ID app registration and its roles +## Dashboard operations + +- **Integrated local build**: run `npm ci` and `VITE_DATA_MODE=live VITE_AUTH_MODE=demo npm run build` + from `src/SecureFix.Web`, then start `src/SecureFix.Api`. The generated assets are written + to `src/SecureFix.Api/wwwroot` and served at `/`. +- **Hot reload**: run `npm run dev` in `src/SecureFix.Web`; Vite proxies API calls to the + configured ASP.NET development URL. +- **Production Entra build**: pass `VITE_AUTH_MODE=entra`, `VITE_ENTRA_CLIENT_ID`, + `VITE_ENTRA_TENANT_ID`, `VITE_ENTRA_API_SCOPE`, and the optional redirect URI as Docker + build arguments. These are public browser configuration, not secrets. +- **Dashboard unavailable**: verify that `wwwroot/index.html` exists in the published image, + request `/health` to separate static-hosting failures from API failures, and check the + browser console for CSP or authentication errors. + ## Identity operations - **Provision app registration and roles**: `./infra/entra-app-registration.sh "SecureFix-AI-API"`. diff --git a/docs/security-model.md b/docs/security-model.md index e3058a0..8d2e389 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -29,6 +29,12 @@ App roles are defined once on the Entra ID app registration (see `infra/entra-app-registration.sh`) and assigned to users/groups from the Enterprise Application's "Users and groups" blade — never granted by the application itself. +The integrated dashboard uses an MSAL browser adapter when built with `VITE_AUTH_MODE=entra`. +Only public identifiers and API scopes are compiled into browser assets; client secrets are +never used by or exposed to the dashboard. In demo mode, the visible role switcher is a local +UX aid backed by the intentionally weak demo headers. Hiding or disabling a UI action is not +a security boundary: every operation remains protected by API role authorization. + ## Roles - Admin @@ -36,8 +42,16 @@ Application's "Users and groups" blade — never granted by the application itse - Developer - Viewer +## Browser controls + +The API serves the dashboard with a restrictive Content Security Policy, clickjacking +protection, MIME-sniffing protection, and a strict referrer policy. Static dashboard routes +are anonymous so the browser can load the application shell; protected data and mutations +still require authenticated API calls. + ## Secrets Secrets must come from environment variables or managed secret storage in production. -No credentials are committed to source control. +No credentials are committed to source control. `VITE_*` values are public build-time +configuration and must never contain secrets. diff --git a/src/SecureFix.Api/Controllers/DashboardController.cs b/src/SecureFix.Api/Controllers/DashboardController.cs new file mode 100644 index 0000000..479f12e --- /dev/null +++ b/src/SecureFix.Api/Controllers/DashboardController.cs @@ -0,0 +1,28 @@ +namespace SecureFix.Api.Controllers; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SecureFix.Core.Models; +using SecureFix.Core.Services; + +[ApiController] +[Route("api/v1/dashboard")] +[Produces("application/json")] +public sealed class DashboardController : ControllerBase +{ + private readonly IDashboardQueryService _dashboardQueryService; + + public DashboardController(IDashboardQueryService dashboardQueryService) + { + _dashboardQueryService = dashboardQueryService ?? + throw new ArgumentNullException(nameof(dashboardQueryService)); + } + + [Authorize(Roles = "Viewer,Developer,SecurityReviewer,Admin")] + [HttpGet("summary")] + [ProducesResponseType(typeof(DashboardSummaryDto), StatusCodes.Status200OK)] + public async Task> GetSummary(CancellationToken cancellationToken) + { + return Ok(await _dashboardQueryService.GetSummaryAsync(cancellationToken)); + } +} diff --git a/src/SecureFix.Api/Controllers/WorkflowsController.cs b/src/SecureFix.Api/Controllers/WorkflowsController.cs index a10ecd7..52f659f 100644 --- a/src/SecureFix.Api/Controllers/WorkflowsController.cs +++ b/src/SecureFix.Api/Controllers/WorkflowsController.cs @@ -16,16 +16,43 @@ namespace SecureFix.Api.Controllers; public class WorkflowsController : ControllerBase { private readonly IApprovalService _approvalService; + private readonly IDashboardQueryService _dashboardQueryService; private readonly ILogger _logger; public WorkflowsController( IApprovalService approvalService, + IDashboardQueryService dashboardQueryService, ILogger logger) { _approvalService = approvalService ?? throw new ArgumentNullException(nameof(approvalService)); + _dashboardQueryService = dashboardQueryService ?? + throw new ArgumentNullException(nameof(dashboardQueryService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + [Authorize(Roles = "Viewer,Developer,SecurityReviewer,Admin")] + [HttpGet] + [ProducesResponseType(typeof(WorkflowListResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + public async Task> GetWorkflows( + [FromQuery] WorkflowListQueryDto query, + CancellationToken cancellationToken) + { + try + { + return Ok(await _dashboardQueryService.GetWorkflowsAsync(query, cancellationToken)); + } + catch (ArgumentException exception) + { + ModelState.AddModelError(exception.ParamName ?? "query", exception.Message); + return BadRequest(new ValidationProblemDetails(ModelState) + { + Status = StatusCodes.Status400BadRequest, + Title = "Invalid workflow query" + }); + } + } + /// /// Get workflow status for an alert. /// @@ -34,7 +61,7 @@ public WorkflowsController( /// Workflow found and returned. /// Workflow not found. /// Server error during lookup. - [Authorize(Roles = "Developer,SecurityReviewer,Admin")] + [Authorize(Roles = "Viewer,Developer,SecurityReviewer,Admin")] [HttpGet("{id}")] [ProducesResponseType(typeof(WorkflowStatusResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/SecureFix.Api/Program.cs b/src/SecureFix.Api/Program.cs index d1ad899..9962c49 100644 --- a/src/SecureFix.Api/Program.cs +++ b/src/SecureFix.Api/Program.cs @@ -55,6 +55,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -87,6 +88,23 @@ var app = builder.Build(); + app.Use(async (context, next) => + { + context.Response.OnStarting(() => + { + context.Response.Headers.TryAdd("X-Content-Type-Options", "nosniff"); + context.Response.Headers.TryAdd("X-Frame-Options", "DENY"); + context.Response.Headers.TryAdd("Referrer-Policy", "strict-origin-when-cross-origin"); + context.Response.Headers.TryAdd( + "Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; " + + "connect-src 'self' https://login.microsoftonline.com; " + + "frame-src https://login.microsoftonline.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"); + return Task.CompletedTask; + }); + await next(); + }); + using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); @@ -99,7 +117,9 @@ app.MapOpenApi(); } - app.UseHttpsRedirection(); + //app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); app.UseCors("SecureFixDemo"); app.UseAuthentication(); app.UseAuthorization(); @@ -139,6 +159,26 @@ .RequireAuthorization(new AuthorizeAttribute { Roles = "Admin" }) .WithName("Metrics"); + app.MapFallback(async context => + { + var isReservedPath = context.Request.Path.StartsWithSegments("/api") || + context.Request.Path.StartsWithSegments("/health") || + context.Request.Path.StartsWithSegments("/ready") || + context.Request.Path.StartsWithSegments("/metrics") || + context.Request.Path.StartsWithSegments("/openapi"); + var indexPath = Path.Combine(app.Environment.WebRootPath ?? string.Empty, "index.html"); + + if ((!HttpMethods.IsGet(context.Request.Method) && !HttpMethods.IsHead(context.Request.Method)) || + isReservedPath || !File.Exists(indexPath)) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + context.Response.ContentType = "text/html; charset=utf-8"; + await context.Response.SendFileAsync(indexPath); + }); + app.Run(); } catch (Exception ex) diff --git a/src/SecureFix.Core/Models/DashboardDtos.cs b/src/SecureFix.Core/Models/DashboardDtos.cs new file mode 100644 index 0000000..e253143 --- /dev/null +++ b/src/SecureFix.Core/Models/DashboardDtos.cs @@ -0,0 +1,70 @@ +namespace SecureFix.Core.Models; + +using System.ComponentModel.DataAnnotations; + +public sealed class WorkflowListQueryDto +{ + [Range(1, int.MaxValue)] + public int Page { get; set; } = 1; + + [Range(1, 100)] + public int PageSize { get; set; } = 20; + + [StringLength(100)] + public string? Search { get; set; } + + [StringLength(20)] + public string? Severity { get; set; } + + [StringLength(30)] + public string? Status { get; set; } + + [StringLength(30)] + public string SortBy { get; set; } = "receivedAt"; + + [StringLength(4)] + public string SortDirection { get; set; } = "desc"; +} + +public sealed class WorkflowListResponseDto +{ + public IReadOnlyList Items { get; set; } = []; + public int Page { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + public int TotalPages { get; set; } +} + +public sealed class WorkflowListItemDto +{ + public string WorkflowId { get; set; } = null!; + public string CorrelationId { get; set; } = null!; + public string ExternalAlertId { get; set; } = null!; + public string? CveId { get; set; } + public string PackageName { get; set; } = null!; + public string InstalledVersion { get; set; } = null!; + public string? FixedVersion { get; set; } + public string? RepositoryIdentifier { get; set; } + public Severity? Severity { get; set; } + public int? RiskScore { get; set; } + public WorkflowStatus Status { get; set; } + public string? RecommendedAction { get; set; } + public string? Reviewer { get; set; } + public DateTimeOffset ReceivedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } +} + +public sealed class DashboardSummaryDto +{ + public int TotalWorkflows { get; set; } + public int PendingApprovalWorkflows { get; set; } + public int ApprovedWorkflows { get; set; } + public int RejectedWorkflows { get; set; } + public int CriticalWorkflows { get; set; } + public int HighWorkflows { get; set; } + public int MediumWorkflows { get; set; } + public int LowWorkflows { get; set; } + public int WorkflowsWithRecommendations { get; set; } + public double AverageRiskScore { get; set; } + public DateTimeOffset GeneratedAt { get; set; } +} diff --git a/src/SecureFix.Core/Services/IDashboardQueryService.cs b/src/SecureFix.Core/Services/IDashboardQueryService.cs new file mode 100644 index 0000000..dbc6a04 --- /dev/null +++ b/src/SecureFix.Core/Services/IDashboardQueryService.cs @@ -0,0 +1,376 @@ +namespace SecureFix.Core.Services; + +using Microsoft.EntityFrameworkCore; +using SecureFix.Core.Data; +using SecureFix.Core.Models; + +public interface IDashboardQueryService +{ + Task GetWorkflowsAsync( + WorkflowListQueryDto query, + CancellationToken cancellationToken = default); + + Task GetSummaryAsync(CancellationToken cancellationToken = default); +} + +public sealed class DashboardQueryService : IDashboardQueryService +{ + private const int MaximumPageSize = 100; + private const int MaximumSearchLength = 100; + private readonly SecureFixDbContext _dbContext; + + public DashboardQueryService(SecureFixDbContext dbContext) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + } + + public async Task GetWorkflowsAsync( + WorkflowListQueryDto query, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(query); + + var page = Math.Max(1, query.Page); + var pageSize = Math.Clamp(query.PageSize, 1, MaximumPageSize); + var search = NormalizeSearch(query.Search); + var severity = ParseOptionalEnum(query.Severity, nameof(query.Severity)); + var status = ParseOptionalEnum(query.Status, nameof(query.Status)); + var sortBy = NormalizeSortBy(query.SortBy); + var descending = NormalizeSortDirection(query.SortDirection); + + var workflows = CreateWorkflowQuery(); + + if (search is not null) + { + var pattern = $"%{EscapeLikePattern(search.ToLowerInvariant())}%"; + workflows = workflows.Where(workflow => + EF.Functions.Like(workflow.WorkflowId.ToLower(), pattern, "\\") || + EF.Functions.Like(workflow.ExternalAlertId.ToLower(), pattern, "\\") || + EF.Functions.Like(workflow.PackageName.ToLower(), pattern, "\\") || + (workflow.CveId != null && EF.Functions.Like(workflow.CveId.ToLower(), pattern, "\\")) || + (workflow.RepositoryIdentifier != null && + EF.Functions.Like(workflow.RepositoryIdentifier.ToLower(), pattern, "\\"))); + } + + if (severity.HasValue) + { + workflows = workflows.Where(workflow => workflow.Severity == severity); + } + + if (status.HasValue) + { + workflows = workflows.Where(workflow => workflow.Status == status); + } + + var totalCount = await workflows.CountAsync(cancellationToken); + var orderedWorkflows = ApplySorting(workflows, sortBy, descending); + var items = await orderedWorkflows + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(workflow => new WorkflowListItemDto + { + WorkflowId = workflow.WorkflowId, + CorrelationId = workflow.CorrelationId, + ExternalAlertId = workflow.ExternalAlertId, + CveId = workflow.CveId, + PackageName = workflow.PackageName, + InstalledVersion = workflow.InstalledVersion, + FixedVersion = workflow.FixedVersion, + RepositoryIdentifier = workflow.RepositoryIdentifier, + Severity = workflow.Severity, + RiskScore = workflow.RiskScore, + Status = workflow.Status, + RecommendedAction = workflow.RecommendedAction, + Reviewer = workflow.Reviewer, + ReceivedAt = workflow.ReceivedAt, + UpdatedAt = workflow.UpdatedAt + }) + .ToListAsync(cancellationToken); + + return new WorkflowListResponseDto + { + Items = items, + Page = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize) + }; + } + + public async Task GetSummaryAsync(CancellationToken cancellationToken = default) + { + var aggregate = await CreateWorkflowQuery() + .GroupBy(_ => 1) + .Select(group => new + { + Total = group.Count(), + Pending = group.Count(workflow => workflow.Status == WorkflowStatus.PendingApproval), + Approved = group.Count(workflow => workflow.Status == WorkflowStatus.Approved), + Rejected = group.Count(workflow => workflow.Status == WorkflowStatus.Rejected), + Critical = group.Count(workflow => workflow.Severity == Severity.Critical), + High = group.Count(workflow => workflow.Severity == Severity.High), + Medium = group.Count(workflow => workflow.Severity == Severity.Medium), + Low = group.Count(workflow => workflow.Severity == Severity.Low), + WithRecommendations = group.Count(workflow => workflow.RecommendedAction != null), + AverageRiskScore = group + .Where(workflow => workflow.RiskScore != null) + .Average(workflow => (double?)workflow.RiskScore) ?? 0 + }) + .SingleOrDefaultAsync(cancellationToken); + + return new DashboardSummaryDto + { + TotalWorkflows = aggregate?.Total ?? 0, + PendingApprovalWorkflows = aggregate?.Pending ?? 0, + ApprovedWorkflows = aggregate?.Approved ?? 0, + RejectedWorkflows = aggregate?.Rejected ?? 0, + CriticalWorkflows = aggregate?.Critical ?? 0, + HighWorkflows = aggregate?.High ?? 0, + MediumWorkflows = aggregate?.Medium ?? 0, + LowWorkflows = aggregate?.Low ?? 0, + WorkflowsWithRecommendations = aggregate?.WithRecommendations ?? 0, + AverageRiskScore = aggregate?.AverageRiskScore ?? 0, + GeneratedAt = DateTimeOffset.UtcNow + }; + } + + private IQueryable CreateWorkflowQuery() + { + return _dbContext.VulnerabilityAlerts + .AsNoTracking() + .Select(alert => new WorkflowQueryRow + { + WorkflowId = alert.Id, + CorrelationId = alert.CorrelationId, + ExternalAlertId = alert.ExternalAlertId, + CveId = alert.CveId, + PackageName = alert.PackageName, + InstalledVersion = alert.InstalledVersion, + FixedVersion = alert.FixedVersion, + RepositoryIdentifier = alert.RepositoryIdentifier, + Severity = _dbContext.RiskAssessments + .AsNoTracking() + .Where(assessment => assessment.AlertId == alert.Id) + .OrderByDescending(assessment => assessment.AssessedAt.ToString()) + .ThenByDescending(assessment => assessment.Id) + .Select(assessment => (Severity?)assessment.NormalizedSeverity) + .FirstOrDefault(), + RiskScore = _dbContext.RiskAssessments + .AsNoTracking() + .Where(assessment => assessment.AlertId == alert.Id) + .OrderByDescending(assessment => assessment.AssessedAt.ToString()) + .ThenByDescending(assessment => assessment.Id) + .Select(assessment => (int?)assessment.RiskScore) + .FirstOrDefault(), + DecisionStatus = _dbContext.ApprovalDecisions + .AsNoTracking() + .Where(decision => decision.AlertId == alert.Id) + .OrderByDescending(decision => decision.DecisionTime.ToString()) + .ThenByDescending(decision => decision.Id) + .Select(decision => (ApprovalStatus?)decision.Status) + .FirstOrDefault(), + Reviewer = _dbContext.ApprovalDecisions + .AsNoTracking() + .Where(decision => decision.AlertId == alert.Id) + .OrderByDescending(decision => decision.DecisionTime.ToString()) + .ThenByDescending(decision => decision.Id) + .Select(decision => decision.ReviewerIdentity) + .FirstOrDefault(), + RecommendedAction = _dbContext.RemediationRecommendations + .AsNoTracking() + .Where(recommendation => recommendation.AlertId == alert.Id) + .OrderByDescending(recommendation => recommendation.GeneratedAt.ToString()) + .ThenByDescending(recommendation => recommendation.Id) + .Select(recommendation => recommendation.RecommendedAction) + .FirstOrDefault(), + RecommendationGeneratedAt = _dbContext.RemediationRecommendations + .AsNoTracking() + .Where(recommendation => recommendation.AlertId == alert.Id) + .OrderByDescending(recommendation => recommendation.GeneratedAt.ToString()) + .ThenByDescending(recommendation => recommendation.Id) + .Select(recommendation => (DateTimeOffset?)recommendation.GeneratedAt) + .FirstOrDefault(), + AssessmentAt = _dbContext.RiskAssessments + .AsNoTracking() + .Where(assessment => assessment.AlertId == alert.Id) + .OrderByDescending(assessment => assessment.AssessedAt.ToString()) + .ThenByDescending(assessment => assessment.Id) + .Select(assessment => (DateTimeOffset?)assessment.AssessedAt) + .FirstOrDefault(), + DecisionAt = _dbContext.ApprovalDecisions + .AsNoTracking() + .Where(decision => decision.AlertId == alert.Id) + .OrderByDescending(decision => decision.DecisionTime.ToString()) + .ThenByDescending(decision => decision.Id) + .Select(decision => (DateTimeOffset?)decision.DecisionTime) + .FirstOrDefault(), + ReceivedAt = alert.ReceivedAt + }) + .Select(workflow => new WorkflowQueryRow + { + WorkflowId = workflow.WorkflowId, + CorrelationId = workflow.CorrelationId, + ExternalAlertId = workflow.ExternalAlertId, + CveId = workflow.CveId, + PackageName = workflow.PackageName, + InstalledVersion = workflow.InstalledVersion, + FixedVersion = workflow.FixedVersion, + RepositoryIdentifier = workflow.RepositoryIdentifier, + Severity = workflow.Severity, + RiskScore = workflow.RiskScore, + DecisionStatus = workflow.DecisionStatus, + Status = workflow.DecisionStatus == ApprovalStatus.Approved + ? WorkflowStatus.Approved + : workflow.DecisionStatus == ApprovalStatus.Rejected + ? WorkflowStatus.Rejected + : workflow.RiskScore != null + ? WorkflowStatus.PendingApproval + : WorkflowStatus.Received, + RecommendedAction = workflow.RecommendedAction, + Reviewer = workflow.Reviewer, + ReceivedAt = workflow.ReceivedAt, + UpdatedAt = workflow.DecisionAt ?? + workflow.RecommendationGeneratedAt ?? + workflow.AssessmentAt ?? + workflow.ReceivedAt + }); + } + + private static IOrderedQueryable ApplySorting( + IQueryable workflows, + string sortBy, + bool descending) + { + return sortBy switch + { + "severity" => descending + ? workflows.OrderByDescending(workflow => workflow.Severity) + .ThenByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId) + : workflows.OrderBy(workflow => workflow.Severity) + .ThenByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId), + "riskscore" => descending + ? workflows.OrderByDescending(workflow => workflow.RiskScore) + .ThenByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId) + : workflows.OrderBy(workflow => workflow.RiskScore) + .ThenByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId), + "status" => descending + ? workflows.OrderByDescending(workflow => workflow.Status) + .ThenByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId) + : workflows.OrderBy(workflow => workflow.Status) + .ThenByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId), + "packagename" => descending + ? workflows.OrderByDescending(workflow => workflow.PackageName) + .ThenBy(workflow => workflow.WorkflowId) + : workflows.OrderBy(workflow => workflow.PackageName) + .ThenBy(workflow => workflow.WorkflowId), + _ => descending + ? workflows.OrderByDescending(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId) + : workflows.OrderBy(workflow => workflow.ReceivedAt.ToString()) + .ThenBy(workflow => workflow.WorkflowId) + }; + } + + private static string? NormalizeSearch(string? search) + { + if (string.IsNullOrWhiteSpace(search)) + { + return null; + } + + var normalized = search.Trim(); + if (normalized.Length > MaximumSearchLength) + { + throw new ArgumentException( + $"Search cannot exceed {MaximumSearchLength} characters.", + nameof(search)); + } + + return normalized; + } + + private static TEnum? ParseOptionalEnum(string? value, string parameterName) + where TEnum : struct, Enum + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + if (!Enum.TryParse(value.Trim(), true, out var parsed) || + !Enum.IsDefined(parsed)) + { + throw new ArgumentException( + $"'{value}' is not a valid {typeof(TEnum).Name}.", + parameterName); + } + + return parsed; + } + + private static string NormalizeSortBy(string? sortBy) + { + var normalized = string.IsNullOrWhiteSpace(sortBy) + ? "receivedat" + : sortBy.Trim().ToLowerInvariant(); + + return normalized is "receivedat" or "severity" or "riskscore" or "status" or "packagename" + ? normalized + : throw new ArgumentException( + "SortBy must be one of: receivedAt, severity, riskScore, status, packageName.", + nameof(sortBy)); + } + + private static bool NormalizeSortDirection(string? sortDirection) + { + var normalized = string.IsNullOrWhiteSpace(sortDirection) + ? "desc" + : sortDirection.Trim().ToLowerInvariant(); + + return normalized switch + { + "asc" => false, + "desc" => true, + _ => throw new ArgumentException( + "SortDirection must be either 'asc' or 'desc'.", + nameof(sortDirection)) + }; + } + + private static string EscapeLikePattern(string value) + { + return value + .Replace("\\", "\\\\", StringComparison.Ordinal) + .Replace("%", "\\%", StringComparison.Ordinal) + .Replace("_", "\\_", StringComparison.Ordinal); + } + + private sealed class WorkflowQueryRow + { + public string WorkflowId { get; set; } = null!; + public string CorrelationId { get; set; } = null!; + public string ExternalAlertId { get; set; } = null!; + public string? CveId { get; set; } + public string PackageName { get; set; } = null!; + public string InstalledVersion { get; set; } = null!; + public string? FixedVersion { get; set; } + public string? RepositoryIdentifier { get; set; } + public Severity? Severity { get; set; } + public int? RiskScore { get; set; } + public ApprovalStatus? DecisionStatus { get; set; } + public WorkflowStatus Status { get; set; } + public string? RecommendedAction { get; set; } + public string? Reviewer { get; set; } + public DateTimeOffset ReceivedAt { get; set; } + public DateTimeOffset? AssessmentAt { get; set; } + public DateTimeOffset? RecommendationGeneratedAt { get; set; } + public DateTimeOffset? DecisionAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + } +} diff --git a/src/SecureFix.Web/.env.example b/src/SecureFix.Web/.env.example new file mode 100644 index 0000000..4f7f54c --- /dev/null +++ b/src/SecureFix.Web/.env.example @@ -0,0 +1,11 @@ +# Use "live" for the integrated dashboard; "demo" enables browser-only sample data. +VITE_DATA_MODE=live +VITE_API_BASE_URL= +VITE_DEMO_TOKEN=securefix-demo-token + +# production Microsoft Entra ID / MSAL configuration (public identifiers, never secrets) +VITE_AUTH_MODE=demo +VITE_ENTRA_CLIENT_ID= +VITE_ENTRA_TENANT_ID= +VITE_ENTRA_API_SCOPE= +VITE_ENTRA_REDIRECT_URI= diff --git a/src/SecureFix.Web/.gitignore b/src/SecureFix.Web/.gitignore new file mode 100644 index 0000000..058da60 --- /dev/null +++ b/src/SecureFix.Web/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +*.local +*.tsbuildinfo +vite.config.js +vite.config.d.ts diff --git a/src/SecureFix.Web/index.html b/src/SecureFix.Web/index.html new file mode 100644 index 0000000..f0a340b --- /dev/null +++ b/src/SecureFix.Web/index.html @@ -0,0 +1,21 @@ + + + + + + + + SecureFix AI + + +
+ + + diff --git a/src/SecureFix.Web/package-lock.json b/src/SecureFix.Web/package-lock.json new file mode 100644 index 0000000..544ac85 --- /dev/null +++ b/src/SecureFix.Web/package-lock.json @@ -0,0 +1,3189 @@ +{ + "name": "securefix-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "securefix-web", + "version": "0.1.0", + "dependencies": { + "@azure/msal-browser": "^4.22.0", + "lucide-react": "^0.542.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "jsdom": "^26.1.0", + "typescript": "~5.8.3", + "vite": "^7.1.4", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@azure/msal-browser": { + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.30.0.tgz", + "integrity": "sha512-HBBKfbZkMVzzF5bofvS1cXuNHFVc+gt4/HOnCmG/0hsHuZRJvJvDg/+7nTwIpoqvJc8BQp5o23rBUfisOLxR+w==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.17.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.17.0.tgz", + "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "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.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.425", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.425.tgz", + "integrity": "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.542.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz", + "integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz", + "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "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", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "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-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/strip-literal/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/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "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.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/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.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.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/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "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.7", + "@vitest/ui": "3.2.7", + "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/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "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", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/src/SecureFix.Web/package.json b/src/SecureFix.Web/package.json new file mode 100644 index 0000000..c5ed403 --- /dev/null +++ b/src/SecureFix.Web/package.json @@ -0,0 +1,32 @@ +{ + "name": "securefix-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@azure/msal-browser": "^4.22.0", + "lucide-react": "^0.542.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "jsdom": "^26.1.0", + "typescript": "~5.8.3", + "vite": "^7.1.4", + "vitest": "^3.2.4" + } +} diff --git a/src/SecureFix.Web/src/App.tsx b/src/SecureFix.Web/src/App.tsx new file mode 100644 index 0000000..53cc774 --- /dev/null +++ b/src/SecureFix.Web/src/App.tsx @@ -0,0 +1,25 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { Shell } from "./components/Shell"; +import { AppProvider } from "./context/AppContext"; +import { IngestPage } from "./pages/IngestPage"; +import { OverviewPage } from "./pages/OverviewPage"; +import { WorkflowDetailPage } from "./pages/WorkflowDetailPage"; +import { WorkflowsPage } from "./pages/WorkflowsPage"; + +export default function App() { + return ( + + + + }> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} diff --git a/src/SecureFix.Web/src/api/client.ts b/src/SecureFix.Web/src/api/client.ts new file mode 100644 index 0000000..48f3505 --- /dev/null +++ b/src/SecureFix.Web/src/api/client.ts @@ -0,0 +1,114 @@ +import type { + AlertIngestionRequest, + AlertIngestionResponse, + ApiErrorShape, + AuditEvent, + DashboardSummary, + GovernanceReport, + Identity, + PullRequestProposal, + RemediationRecommendation, + Workflow, + WorkflowListResponse, +} from "./types"; +import type { AuthAdapter } from "../auth/auth"; + +export class ApiError extends Error { + constructor( + message: string, + public readonly status: number, + public readonly details?: ApiErrorShape, + ) { + super(message); + } +} + +export class SecureFixApiClient { + constructor( + private readonly auth: AuthAdapter, + private readonly getIdentity: () => Identity, + private readonly baseUrl = import.meta.env.VITE_API_BASE_URL || "", + ) {} + + getWorkflow(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}`); + } + + getWorkflows(page = 1, pageSize = 100) { + return this.request(`/api/v1/workflows?page=${page}&pageSize=${pageSize}`); + } + + getDashboardSummary() { + return this.request("/api/v1/dashboard/summary"); + } + + ingestAlert(payload: AlertIngestionRequest) { + return this.request("/api/v1/alerts", { + method: "POST", + body: JSON.stringify(payload), + }); + } + + decideWorkflow(id: string, decision: "approved" | "rejected", reason?: string) { + const identity = this.getIdentity(); + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/${decision === "approved" ? "approve" : "reject"}`, { + method: "POST", + body: JSON.stringify({ + reviewer: identity.email, + reviewerRole: identity.role, + decision, + reason, + }), + }); + } + + generateRemediation(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/remediate`, { + method: "POST", + }); + } + + getRemediation(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/remediate`); + } + + generateProposal(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/proposal`, { + method: "POST", + }); + } + + getProposal(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/proposal`); + } + + getAuditEvents(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/audit-events`); + } + + getGovernanceReport(id: string) { + return this.request(`/api/v1/workflows/${encodeURIComponent(id)}/governance-report`); + } + + private async request(path: string, init: RequestInit = {}): Promise { + const identity = this.getIdentity(); + const token = await this.auth.getAccessToken(); + const headers = new Headers(init.headers); + headers.set("Accept", "application/json"); + headers.set("Content-Type", "application/json"); + headers.set("Authorization", `Bearer ${token}`); + if (identity.authMode === "demo") { + headers.set("X-User-Id", identity.email); + headers.set("X-User-Role", identity.role); + } + const response = await fetch(`${this.baseUrl}${path}`, { + ...init, + headers, + }); + if (!response.ok) { + const details = (await response.json().catch(() => undefined)) as ApiErrorShape | undefined; + throw new ApiError(details?.detail || details?.message || details?.title || `Request failed (${response.status})`, response.status, details); + } + return (await response.json()) as T; + } +} diff --git a/src/SecureFix.Web/src/api/types.ts b/src/SecureFix.Web/src/api/types.ts new file mode 100644 index 0000000..ee2c962 --- /dev/null +++ b/src/SecureFix.Web/src/api/types.ts @@ -0,0 +1,201 @@ +export type Role = "Admin" | "SecurityReviewer" | "Developer" | "Viewer"; +export type Severity = "Critical" | "High" | "Medium" | "Low"; +export type WorkflowStatus = + | "Received" + | "Validated" + | "Assessed" + | "Recommended" + | "PendingApproval" + | "Approved" + | "Rejected" + | "Failed"; +export type ApprovalStatus = "Pending" | "Approved" | "Rejected"; + +export interface Identity { + name: string; + email: string; + role: Role; + authMode: "demo" | "entra"; +} + +export interface AlertIngestionRequest { + externalAlertId: string; + cveId?: string; + packageName: string; + installedVersion: string; + fixedVersion?: string; + providerSeverity: string; + description?: string; + isDirectDependency?: boolean; + isExploitable?: boolean; + repositoryIdentifier?: string; + advisoryUrl?: string; +} + +export interface RiskAssessment { + id?: string; + alertId?: string; + riskScore: number; + normalizedSeverity: Severity | number; + confidenceScore?: number; + requiredApprovalLevel: string; + riskFactors: string[]; +} + +export interface AlertIngestionResponse { + workflowId: string; + correlationId: string; + isAccepted: boolean; + assessment: RiskAssessment; + nextStep: string; + message?: string; + receivedAt: string; +} + +export interface Workflow { + workflowId: string; + correlationId: string; + status: WorkflowStatus | number; + alert?: { + id: string; + packageName: string; + installedVersion: string; + fixedVersion?: string; + providerSeverity: string; + cveId?: string; + description?: string; + }; + riskAssessment?: RiskAssessment; + approval?: { + status: ApprovalStatus | number; + reviewer?: string; + reason?: string; + decidedAt?: string; + }; + remediation?: { + recommendedAction: string; + targetVersion?: string; + confidenceScore: number; + modelIdentifier: string; + }; + nextStep?: string; + createdAt: string; + updatedAt: string; + repository?: string; +} + +export interface WorkflowListItem { + workflowId: string; + correlationId: string; + externalAlertId: string; + cveId?: string; + packageName: string; + installedVersion: string; + fixedVersion?: string; + repositoryIdentifier?: string; + severity?: Severity | number; + riskScore?: number; + status: WorkflowStatus | number; + recommendedAction?: string; + reviewer?: string; + receivedAt: string; + updatedAt: string; +} + +export interface WorkflowListResponse { + items: WorkflowListItem[]; + page: number; + pageSize: number; + totalCount: number; + totalPages: number; +} + +export interface DashboardSummary { + totalWorkflows: number; + pendingApprovalWorkflows: number; + approvedWorkflows: number; + rejectedWorkflows: number; + criticalWorkflows: number; + highWorkflows: number; + mediumWorkflows: number; + lowWorkflows: number; + workflowsWithRecommendations: number; + averageRiskScore: number; + generatedAt: string; +} + +export interface RemediationRecommendation { + id: string; + alertId: string; + riskAssessmentId: string; + correlationId: string; + recommendedAction: string; + targetVersion?: string; + explanation: string; + assumptions?: string; + confidenceScore: number; + modelIdentifier: string; + promptVersion?: string; + requiresHumanReview: boolean; + potentialRisks?: string; + alternativeActions: string[]; + generatedAt: string; +} + +export interface PullRequestProposal { + id: string; + recommendationId: string; + alertId: string; + correlationId: string; + proposedTitle: string; + proposedDescription: string; + filesForReview: string[]; + dependencyChanges: string[]; + validationCommands: string[]; + rollbackGuidance?: string; + knownLimitations?: string; + resourceLinks: string[]; + estimatedEffort?: string; + isReadyForReview: boolean; + rawProposalJson?: string; + generatedAt: string; +} + +export interface AuditEvent { + id: string; + correlationId: string; + workflowId?: string; + eventType: string; + level: string; + summary: string; + details?: string; + actor?: string; + service?: string; + alertId?: string; + metadata?: string; + timestamp: string; + isSecurityRelevant: boolean; +} + +export interface GovernanceReport { + alertId: string; + correlationId: string; + severity: number; + requiredApprovalLevel: string; + approvalStatus: string; + reviewer?: string; + approvalTimestamp?: string; + recommendedAction?: string; + recommendationConfidence?: number; + modelName?: string; + promptVersion?: string; + generatedAt: string; +} + +export interface ApiErrorShape { + title?: string; + detail?: string; + status?: number; + errors?: Record; + message?: string; +} diff --git a/src/SecureFix.Web/src/auth/auth.ts b/src/SecureFix.Web/src/auth/auth.ts new file mode 100644 index 0000000..d2e43e5 --- /dev/null +++ b/src/SecureFix.Web/src/auth/auth.ts @@ -0,0 +1,104 @@ +import { + PublicClientApplication, + type AccountInfo, + type AuthenticationResult, + type Configuration, +} from "@azure/msal-browser"; +import type { Identity, Role } from "../api/types"; + +export interface AuthAdapter { + initialize(): Promise; + getAccessToken(): Promise; + signIn(): Promise; + signOut(): Promise; +} + +const configuredRole = (role: string | undefined): Role => + ["Admin", "SecurityReviewer", "Developer", "Viewer"].includes(role ?? "") + ? (role as Role) + : "Developer"; + +export class DemoAuthAdapter implements AuthAdapter { + constructor(private identity: Identity) {} + + async initialize() { + return this.identity; + } + + async getAccessToken() { + return import.meta.env.VITE_DEMO_TOKEN || "securefix-demo-token"; + } + + async signIn() { + return this.identity; + } + + async signOut() {} +} + +export class EntraMsalAuthAdapter implements AuthAdapter { + private readonly client: PublicClientApplication; + private account?: AccountInfo; + private readonly scope: string; + + constructor() { + const clientId = import.meta.env.VITE_ENTRA_CLIENT_ID; + const tenantId = import.meta.env.VITE_ENTRA_TENANT_ID; + this.scope = import.meta.env.VITE_ENTRA_API_SCOPE; + if (!clientId || !tenantId || !this.scope) { + throw new Error("Entra auth requires VITE_ENTRA_CLIENT_ID, VITE_ENTRA_TENANT_ID, and VITE_ENTRA_API_SCOPE."); + } + + const config: Configuration = { + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + redirectUri: import.meta.env.VITE_ENTRA_REDIRECT_URI || window.location.origin, + postLogoutRedirectUri: window.location.origin, + }, + cache: { cacheLocation: "sessionStorage" }, + }; + this.client = new PublicClientApplication(config); + } + + async initialize() { + await this.client.initialize(); + const redirect = await this.client.handleRedirectPromise(); + this.account = redirect?.account ?? this.client.getAllAccounts()[0]; + return this.account ? this.toIdentity(this.account, redirect) : this.signIn(); + } + + async signIn() { + const result = await this.client.loginPopup({ scopes: [this.scope] }); + this.account = result.account; + return this.toIdentity(result.account, result); + } + + async getAccessToken() { + if (!this.account) throw new Error("No signed-in Entra account."); + try { + return (await this.client.acquireTokenSilent({ account: this.account, scopes: [this.scope] })).accessToken; + } catch { + return (await this.client.acquireTokenPopup({ account: this.account, scopes: [this.scope] })).accessToken; + } + } + + async signOut() { + await this.client.logoutPopup({ account: this.account }); + } + + private toIdentity(account: AccountInfo, result?: AuthenticationResult | null): Identity { + const claims = (result?.idTokenClaims ?? account.idTokenClaims) as { roles?: string[] } | undefined; + return { + name: account.name || account.username, + email: account.username, + role: configuredRole(claims?.roles?.[0]), + authMode: "entra", + }; + } +} + +export const createAuthAdapter = (identity: Identity): AuthAdapter => + import.meta.env.VITE_AUTH_MODE === "entra" + ? new EntraMsalAuthAdapter() + : new DemoAuthAdapter(identity); diff --git a/src/SecureFix.Web/src/components/Shell.tsx b/src/SecureFix.Web/src/components/Shell.tsx new file mode 100644 index 0000000..0d393f5 --- /dev/null +++ b/src/SecureFix.Web/src/components/Shell.tsx @@ -0,0 +1,96 @@ +import { + Activity, + Bell, + ChevronDown, + FileCheck2, + LayoutDashboard, + Menu, + Plus, + ShieldCheck, + X, +} from "lucide-react"; +import { useState } from "react"; +import { NavLink, Outlet } from "react-router-dom"; +import type { Role } from "../api/types"; +import { useApp } from "../context/AppContext"; + +const navigation = [ + { to: "/", label: "Overview", icon: LayoutDashboard }, + { to: "/workflows", label: "Workflows", icon: Activity }, + { to: "/ingest", label: "Ingest alert", icon: Plus }, +]; + +export function Shell() { + const { identity, setRole, isDemo } = useApp(); + const [mobileOpen, setMobileOpen] = useState(false); + const [identityOpen, setIdentityOpen] = useState(false); + + return ( +
+ Skip to main content + +
+
+ +
+ Security operations + Release assurance +
+
+ +
+ + {identityOpen && ( +
+ {identity.authMode === "demo" ? "Demo identity role" : "Microsoft Entra identity"} +

{identity.email}

+ {identity.authMode === "demo" ? ( + <> + + + Demo roles are self-asserted and must never be enabled in production. + + ) : ( + Role assignments are read from Microsoft Entra app-role claims. + )} +
+ )} +
+
+
+
+
+ {mobileOpen &&
+ ); +} diff --git a/src/SecureFix.Web/src/components/ui.tsx b/src/SecureFix.Web/src/components/ui.tsx new file mode 100644 index 0000000..e68de95 --- /dev/null +++ b/src/SecureFix.Web/src/components/ui.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from "react"; +import type { ApprovalStatus, Severity, WorkflowStatus } from "../api/types"; + +export const severityName = (value: Severity | number | undefined): Severity => { + if (typeof value === "string") return value; + return (["Low", "Low", "Medium", "High", "Critical"][value ?? 1] ?? "Low") as Severity; +}; + +export const statusName = (value: WorkflowStatus | number): WorkflowStatus => { + if (typeof value === "string") return value; + return (["Received", "Received", "Validated", "Assessed", "Recommended", "PendingApproval", "Approved", "Rejected", "Failed"][value] ?? "Received") as WorkflowStatus; +}; + +export const approvalName = (value: ApprovalStatus | number | undefined): ApprovalStatus => { + if (typeof value === "string") return value; + return (["Pending", "Pending", "Approved", "Rejected"][value ?? 1] ?? "Pending") as ApprovalStatus; +}; + +export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: "critical" | "high" | "medium" | "low" | "success" | "danger" | "warning" | "neutral" }) { + return {children}; +} + +export const severityTone = (severity: Severity) => + severity === "Critical" ? "critical" : severity === "High" ? "high" : severity === "Medium" ? "medium" : "low"; + +export function EmptyState({ title, detail, action }: { title: string; detail: string; action?: ReactNode }) { + return ( +
+ +

{title}

+

{detail}

+ {action} +
+ ); +} + +export function LoadingState({ label = "Loading secure workflow data" }: { label?: string }) { + return ( +
+
+ ); +} + +export function ErrorBanner({ message }: { message: string }) { + return
Request failed. {message}
; +} + +export function ProgressBar({ value, label }: { value: number; label: string }) { + return ( +
+ {label}: {value}% + +
+ ); +} diff --git a/src/SecureFix.Web/src/context/AppContext.tsx b/src/SecureFix.Web/src/context/AppContext.tsx new file mode 100644 index 0000000..334917c --- /dev/null +++ b/src/SecureFix.Web/src/context/AppContext.tsx @@ -0,0 +1,248 @@ +import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; +import { SecureFixApiClient } from "../api/client"; +import type { + AlertIngestionRequest, + AuditEvent, + GovernanceReport, + Identity, + PullRequestProposal, + RemediationRecommendation, + Role, + Severity, + Workflow, + WorkflowStatus, +} from "../api/types"; +import { createAuthAdapter } from "../auth/auth"; +import { + createDemoAudit, + createDemoGovernance, + createDemoProposal, + createDemoRemediation, + demoWorkflows, +} from "../data/demo"; + +interface AppContextValue { + identity: Identity; + setRole: (role: Role) => void; + workflows: Workflow[]; + dataLoading: boolean; + dataError: string; + getWorkflow: (id: string) => Workflow | undefined; + ingest: (payload: AlertIngestionRequest) => Promise; + decide: (id: string, decision: "approved" | "rejected", reason: string) => Promise; + generateRemediation: (id: string) => Promise; + generateProposal: (id: string) => Promise; + getAudit: (id: string) => Promise; + getGovernance: (id: string) => Promise; + remediations: Record; + proposals: Record; + isDemo: boolean; +} + +const AppContext = createContext(null); + +const statusName = (status: Workflow["status"]): WorkflowStatus => { + if (typeof status === "string") return status; + return (["Received", "Received", "Validated", "Assessed", "Recommended", "PendingApproval", "Approved", "Rejected", "Failed"][status] ?? "Received") as WorkflowStatus; +}; + +export function AppProvider({ children }: { children: ReactNode }) { + const isDemo = import.meta.env.VITE_DATA_MODE === "demo"; + const [identity, setIdentity] = useState({ + name: "Morgan Lee", + email: "morgan.lee@contoso.com", + role: "SecurityReviewer", + authMode: import.meta.env.VITE_AUTH_MODE === "entra" ? "entra" : "demo", + }); + const [workflows, setWorkflows] = useState(isDemo ? demoWorkflows : []); + const [remediations, setRemediations] = useState>({}); + const [proposals, setProposals] = useState>({}); + const [dataLoading, setDataLoading] = useState(!isDemo); + const [dataError, setDataError] = useState(""); + const auth = useMemo(() => createAuthAdapter(identity), []); + const [authReady, setAuthReady] = useState(identity.authMode === "demo"); + const [authError, setAuthError] = useState(""); + const api = useMemo(() => new SecureFixApiClient(auth, () => identity), [auth, identity]); + + useEffect(() => { + if (identity.authMode !== "entra") return; + void auth.initialize() + .then((authenticatedIdentity) => { + setIdentity(authenticatedIdentity); + setAuthReady(true); + }) + .catch((caught) => setAuthError(caught instanceof Error ? caught.message : "Microsoft Entra sign-in failed.")); + }, [auth, identity.authMode]); + + useEffect(() => { + if (isDemo || !authReady) return; + setDataLoading(true); + void api.getWorkflows() + .then((response) => setWorkflows(response.items.map((item) => ({ + workflowId: item.workflowId, + correlationId: item.correlationId, + status: item.status, + repository: item.repositoryIdentifier, + alert: { + id: item.externalAlertId, + packageName: item.packageName, + installedVersion: item.installedVersion, + fixedVersion: item.fixedVersion, + providerSeverity: String(item.severity ?? ""), + cveId: item.cveId, + }, + riskAssessment: item.riskScore == null || item.severity == null ? undefined : { + riskScore: item.riskScore, + normalizedSeverity: item.severity, + requiredApprovalLevel: "SecurityReviewer", + riskFactors: [], + }, + approval: { + status: statusName(item.status) === "Approved" ? "Approved" : statusName(item.status) === "Rejected" ? "Rejected" : "Pending", + reviewer: item.reviewer, + }, + remediation: item.recommendedAction ? { + recommendedAction: item.recommendedAction, + confidenceScore: 0, + modelIdentifier: "See workflow detail", + } : undefined, + createdAt: item.receivedAt, + updatedAt: item.updatedAt, + })))) + .catch((caught) => setDataError(caught instanceof Error ? caught.message : "Could not load workflows.")) + .finally(() => setDataLoading(false)); + }, [api, authReady, isDemo]); + + const updateWorkflow = (workflow: Workflow) => { + const normalized = { ...workflow, status: statusName(workflow.status) }; + setWorkflows((items) => [normalized, ...items.filter((item) => item.workflowId !== normalized.workflowId)]); + return normalized; + }; + + const ingest = async (payload: AlertIngestionRequest) => { + if (!isDemo) { + const response = await api.ingestAlert(payload); + return updateWorkflow(await api.getWorkflow(response.workflowId)); + } + const now = new Date().toISOString(); + const severity = payload.providerSeverity.charAt(0).toUpperCase() + payload.providerSeverity.slice(1).toLowerCase(); + const workflow: Workflow = { + workflowId: `wf-${crypto.randomUUID().slice(0, 6)}`, + correlationId: crypto.randomUUID(), + status: "PendingApproval", + repository: payload.repositoryIdentifier, + alert: { + id: payload.externalAlertId, + packageName: payload.packageName, + installedVersion: payload.installedVersion, + fixedVersion: payload.fixedVersion, + providerSeverity: payload.providerSeverity, + cveId: payload.cveId, + description: payload.description, + }, + riskAssessment: { + riskScore: severity === "Critical" ? 94 : severity === "High" ? 81 : severity === "Medium" ? 56 : 28, + normalizedSeverity: severity as Severity, + confidenceScore: 0.9, + requiredApprovalLevel: severity === "Critical" ? "Admin or SecurityReviewer" : "SecurityReviewer", + riskFactors: [payload.isExploitable ? "Exploitability reported" : "Published advisory", payload.isDirectDependency ? "Direct dependency" : "Transitive dependency", "Fixed version available"], + }, + approval: { status: "Pending" }, + nextStep: "Human approval required", + createdAt: now, + updatedAt: now, + }; + return updateWorkflow(workflow); + }; + + const decide = async (id: string, decision: "approved" | "rejected", reason: string) => { + if (!isDemo) return updateWorkflow(await api.decideWorkflow(id, decision, reason)); + const workflow = workflows.find((item) => item.workflowId === id); + if (!workflow) throw new Error("Workflow not found."); + return updateWorkflow({ + ...workflow, + status: decision === "approved" ? "Approved" : "Rejected", + approval: { + status: decision === "approved" ? "Approved" : "Rejected", + reviewer: identity.email, + reason, + decidedAt: new Date().toISOString(), + }, + nextStep: decision === "approved" ? "Generate remediation recommendation" : "No action permitted", + updatedAt: new Date().toISOString(), + }); + }; + + const generateRemediation = async (id: string) => { + const workflow = workflows.find((item) => item.workflowId === id); + if (!workflow) throw new Error("Workflow not found."); + const remediation = isDemo ? createDemoRemediation(workflow) : await api.generateRemediation(id); + setRemediations((items) => ({ ...items, [id]: remediation })); + updateWorkflow({ + ...workflow, + remediation: { + recommendedAction: remediation.recommendedAction, + targetVersion: remediation.targetVersion, + confidenceScore: remediation.confidenceScore, + modelIdentifier: remediation.modelIdentifier, + }, + nextStep: "Generate draft pull request proposal", + updatedAt: new Date().toISOString(), + }); + return remediation; + }; + + const generateProposal = async (id: string) => { + const workflow = workflows.find((item) => item.workflowId === id); + if (!workflow) throw new Error("Workflow not found."); + const proposal = isDemo ? createDemoProposal(workflow) : await api.generateProposal(id); + setProposals((items) => ({ ...items, [id]: proposal })); + return proposal; + }; + + if (authError) { + return
Authentication failed{authError}
; + } + + if (!authReady) { + return
; + } + + return ( + setIdentity((current) => ({ ...current, role })), + workflows, + dataLoading, + dataError, + getWorkflow: (id) => workflows.find((item) => item.workflowId === id), + ingest, + decide, + generateRemediation, + generateProposal, + getAudit: async (id) => { + const workflow = workflows.find((item) => item.workflowId === id); + if (!workflow) throw new Error("Workflow not found."); + return isDemo ? createDemoAudit(workflow) : api.getAuditEvents(id); + }, + getGovernance: async (id) => { + const workflow = workflows.find((item) => item.workflowId === id); + if (!workflow) throw new Error("Workflow not found."); + return isDemo ? createDemoGovernance(workflow) : api.getGovernanceReport(id); + }, + remediations, + proposals, + isDemo, + }} + > + {children} + + ); +} + +export const useApp = () => { + const value = useContext(AppContext); + if (!value) throw new Error("useApp must be used inside AppProvider."); + return value; +}; diff --git a/src/SecureFix.Web/src/data/demo.ts b/src/SecureFix.Web/src/data/demo.ts new file mode 100644 index 0000000..b5676ca --- /dev/null +++ b/src/SecureFix.Web/src/data/demo.ts @@ -0,0 +1,199 @@ +import type { + AuditEvent, + GovernanceReport, + PullRequestProposal, + RemediationRecommendation, + Severity, + Workflow, + WorkflowStatus, +} from "../api/types"; + +const hoursAgo = (hours: number) => new Date(Date.now() - hours * 3_600_000).toISOString(); + +const seed = [ + ["wf-8f21", "lodash", "4.17.20", "4.17.21", "CVE-2021-23337", "Critical", 96, "PendingApproval", "commerce/api", 2], + ["wf-7ca9", "Microsoft.Identity.Web", "2.16.0", "3.2.0", "CVE-2024-21319", "High", 82, "Approved", "identity/service", 7], + ["wf-1dd4", "requests", "2.31.0", "2.32.3", "CVE-2024-35195", "High", 78, "Recommended", "automation/worker", 13], + ["wf-94b3", "vite", "5.4.8", "5.4.14", "CVE-2025-30208", "Medium", 58, "Rejected", "portal/web", 22], + ["wf-35af", "System.Text.Json", "8.0.3", "8.0.5", "CVE-2024-43485", "Critical", 92, "Approved", "payments/api", 31], + ["wf-a131", "axios", "1.6.7", "1.7.4", "CVE-2024-39338", "Medium", 54, "Assessed", "mobile/gateway", 45], + ["wf-b290", "cryptography", "41.0.3", "43.0.1", "CVE-2024-6119", "High", 86, "Failed", "ml/inference", 62], + ["wf-c422", "minimist", "1.2.5", "1.2.8", "CVE-2021-44906", "Low", 27, "Received", "tooling/cli", 80], +] as const; + +export const demoWorkflows: Workflow[] = seed.map( + ([id, packageName, installedVersion, fixedVersion, cveId, severity, score, status, repository, age]) => ({ + workflowId: id, + correlationId: `corr-${id.slice(3)}-2026`, + status: status as WorkflowStatus, + repository, + alert: { + id, + packageName, + installedVersion, + fixedVersion, + providerSeverity: severity.toLowerCase(), + cveId, + description: `${packageName} contains a known vulnerability that may affect confidentiality or service integrity. External advisory text is treated as untrusted input.`, + }, + riskAssessment: { + riskScore: score, + normalizedSeverity: severity as Severity, + confidenceScore: 0.91, + requiredApprovalLevel: severity === "Critical" ? "Admin or SecurityReviewer" : "SecurityReviewer", + riskFactors: [ + severity === "Critical" ? "Critical provider severity" : "Published security advisory", + "Direct production dependency", + score > 80 ? "High exploitability signal" : "Fixed version available", + ], + }, + approval: { + status: status === "Approved" ? "Approved" : status === "Rejected" ? "Rejected" : "Pending", + reviewer: status === "Approved" ? "alex.chen@contoso.com" : status === "Rejected" ? "priya.shah@contoso.com" : undefined, + reason: status === "Approved" ? "Validated upgrade path and test coverage." : status === "Rejected" ? "Upgrade requires a breaking framework migration." : undefined, + decidedAt: status === "Approved" || status === "Rejected" ? hoursAgo(age - 1) : undefined, + }, + remediation: + ["Recommended", "Approved"].includes(status) + ? { + recommendedAction: "upgrade_dependency", + targetVersion: fixedVersion, + confidenceScore: 0.92, + modelIdentifier: "azure-openai/gpt-4.1", + } + : undefined, + nextStep: + status === "PendingApproval" + ? "Human approval required" + : status === "Approved" + ? "Generate or review draft proposal" + : status === "Rejected" + ? "No action permitted" + : status === "Failed" + ? "Human review required" + : "Continue security workflow", + createdAt: hoursAgo(age), + updatedAt: hoursAgo(Math.max(age - 1, 0)), + }), +); + +export const createDemoAudit = (workflow: Workflow): AuditEvent[] => { + const approvalStatus = typeof workflow.approval?.status === "string" + ? workflow.approval.status + : workflow.approval?.status === 2 + ? "Approved" + : workflow.approval?.status === 3 + ? "Rejected" + : "Pending"; + const events: AuditEvent[] = [ + { + id: `${workflow.workflowId}-evt-1`, + correlationId: workflow.correlationId, + workflowId: workflow.workflowId, + alertId: workflow.workflowId, + eventType: "AlertReceived", + level: "Info", + summary: "Vulnerability alert received and correlation ID assigned.", + actor: "dependabot", + service: "AlertIngestion", + timestamp: workflow.createdAt, + isSecurityRelevant: true, + }, + { + id: `${workflow.workflowId}-evt-2`, + correlationId: workflow.correlationId, + workflowId: workflow.workflowId, + alertId: workflow.workflowId, + eventType: "RiskAssessed", + level: "Info", + summary: `Risk engine assigned score ${workflow.riskAssessment?.riskScore ?? 0}.`, + actor: "securefix-risk-engine", + service: "RiskEngine", + timestamp: new Date(new Date(workflow.createdAt).getTime() + 42_000).toISOString(), + isSecurityRelevant: true, + }, + ]; + if (workflow.remediation) { + events.push({ + id: `${workflow.workflowId}-evt-3`, + correlationId: workflow.correlationId, + workflowId: workflow.workflowId, + alertId: workflow.workflowId, + eventType: "RecommendationGenerated", + level: "Info", + summary: "Advisory remediation recommendation generated for human review.", + actor: workflow.remediation.modelIdentifier, + service: "AIProvider", + timestamp: hoursAgo(3), + isSecurityRelevant: true, + }); + } + if (approvalStatus !== "Pending") { + events.push({ + id: `${workflow.workflowId}-evt-4`, + correlationId: workflow.correlationId, + workflowId: workflow.workflowId, + alertId: workflow.workflowId, + eventType: approvalStatus === "Approved" ? "ApprovalGranted" : "ApprovalDenied", + level: approvalStatus === "Approved" ? "Info" : "Warning", + summary: `Workflow ${approvalStatus.toLowerCase()} by an authorized human reviewer.`, + details: workflow.approval?.reason, + actor: workflow.approval?.reviewer, + service: "ApprovalGate", + timestamp: workflow.approval?.decidedAt ?? workflow.updatedAt, + isSecurityRelevant: true, + }); + } + return events; +}; + +export const createDemoRemediation = (workflow: Workflow): RemediationRecommendation => ({ + id: `rec-${workflow.workflowId}`, + alertId: workflow.workflowId, + riskAssessmentId: `risk-${workflow.workflowId}`, + correlationId: workflow.correlationId, + recommendedAction: "upgrade_dependency", + targetVersion: workflow.alert?.fixedVersion, + explanation: `Upgrade ${workflow.alert?.packageName} from ${workflow.alert?.installedVersion} to the scanner-provided fixed version ${workflow.alert?.fixedVersion}. Keep the change isolated, run the existing test suite, and verify authentication and authorization paths before release.`, + assumptions: "The fixed version originates from the trusted advisory metadata and is compatible with the current package manifest.", + confidenceScore: 0.92, + modelIdentifier: "azure-openai/gpt-4.1", + promptVersion: "remediation-v1.3", + requiresHumanReview: true, + potentialRisks: "The dependency update may introduce behavioral changes. Automated and reviewer validation remain mandatory.", + alternativeActions: ["Apply a vendor-supported backport", "Temporarily isolate the affected component", "Accept risk with an expiration date"], + generatedAt: new Date().toISOString(), +}); + +export const createDemoProposal = (workflow: Workflow): PullRequestProposal => ({ + id: `proposal-${workflow.workflowId}`, + recommendationId: `rec-${workflow.workflowId}`, + alertId: workflow.workflowId, + correlationId: workflow.correlationId, + proposedTitle: `security: upgrade ${workflow.alert?.packageName} to ${workflow.alert?.fixedVersion}`, + proposedDescription: `## Security remediation\n\nUpgrades **${workflow.alert?.packageName}** from \`${workflow.alert?.installedVersion}\` to \`${workflow.alert?.fixedVersion}\` to address ${workflow.alert?.cveId}.\n\nThis artifact is a draft proposal only. It does not create, approve, merge, deploy, or release a pull request.`, + filesForReview: ["dependency manifest", "dependency lockfile"], + dependencyChanges: [`${workflow.alert?.packageName}: ${workflow.alert?.installedVersion} -> ${workflow.alert?.fixedVersion}`], + validationCommands: ["run unit tests", "run dependency scan", "run build verification"], + rollbackGuidance: `Revert the dependency manifest and lockfile to ${workflow.alert?.installedVersion}, then redeploy the last known-good artifact.`, + knownLimitations: "Compatibility is inferred from available metadata and must be validated by a human reviewer.", + resourceLinks: [], + estimatedEffort: "minimal", + isReadyForReview: true, + generatedAt: new Date().toISOString(), +}); + +export const createDemoGovernance = (workflow: Workflow): GovernanceReport => ({ + alertId: workflow.workflowId, + correlationId: workflow.correlationId, + severity: ["Low", "Medium", "High", "Critical"].indexOf(String(workflow.riskAssessment?.normalizedSeverity)) + 1, + requiredApprovalLevel: workflow.riskAssessment?.requiredApprovalLevel ?? "SecurityReviewer", + approvalStatus: String(workflow.approval?.status ?? "Pending"), + reviewer: workflow.approval?.reviewer, + approvalTimestamp: workflow.approval?.decidedAt, + recommendedAction: workflow.remediation?.recommendedAction, + recommendationConfidence: workflow.remediation?.confidenceScore, + modelName: workflow.remediation?.modelIdentifier, + promptVersion: workflow.remediation ? "remediation-v1.3" : undefined, + generatedAt: new Date().toISOString(), +}); diff --git a/src/SecureFix.Web/src/main.tsx b/src/SecureFix.Web/src/main.tsx new file mode 100644 index 0000000..693ac2e --- /dev/null +++ b/src/SecureFix.Web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/src/SecureFix.Web/src/pages/IngestPage.tsx b/src/SecureFix.Web/src/pages/IngestPage.tsx new file mode 100644 index 0000000..40ff954 --- /dev/null +++ b/src/SecureFix.Web/src/pages/IngestPage.tsx @@ -0,0 +1,88 @@ +import { Braces, CheckCircle2, FileJson, ShieldAlert } from "lucide-react"; +import { useState, type FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import type { AlertIngestionRequest } from "../api/types"; +import { ErrorBanner } from "../components/ui"; +import { useApp } from "../context/AppContext"; + +const sample: AlertIngestionRequest = { + externalAlertId: "dependabot-1842", + cveId: "CVE-2025-30208", + packageName: "vite", + installedVersion: "5.4.8", + fixedVersion: "5.4.14", + providerSeverity: "high", + description: "Development server request handling vulnerability reported by Dependabot. Treat this text as untrusted advisory input.", + isDirectDependency: true, + isExploitable: false, + repositoryIdentifier: "securefix/portal", + advisoryUrl: "https://github.com/advisories/GHSA-x574-m823-4x7w", +}; + +export function IngestPage() { + const { ingest, identity } = useApp(); + const navigate = useNavigate(); + const [mode, setMode] = useState<"form" | "json">("form"); + const [form, setForm] = useState(sample); + const [raw, setRaw] = useState(JSON.stringify(sample, null, 2)); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const canIngest = ["Admin", "SecurityReviewer", "Developer"].includes(identity.role); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setError(""); + setBusy(true); + try { + const payload = mode === "json" ? (JSON.parse(raw) as AlertIngestionRequest) : form; + if (!payload.externalAlertId || !payload.packageName || !payload.installedVersion || !payload.providerSeverity) { + throw new Error("External alert ID, package, installed version, and severity are required."); + } + if (payload.cveId && !/^CVE-\d{4}-\d{4,}$/i.test(payload.cveId)) throw new Error("CVE must use the format CVE-YYYY-NNNN."); + navigate(`/workflows/${(await ingest(payload)).workflowId}`); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Alert ingestion failed."); + } finally { + setBusy(false); + } + }; + + const update = (key: keyof AlertIngestionRequest, value: string | boolean) => setForm((current) => ({ ...current, [key]: value })); + + return ( +
+
Untrusted input boundary

Ingest vulnerability alert

Submit normalized scanner data or paste a Dependabot-compatible payload. All external text is validated and isolated from system instructions.

+
+
+
+ + +
+ {error && } +
+ {mode === "form" ? ( +
+ + + + + + + + +