diff --git a/.bazelignore b/.bazelignore index e46df37b6..c98fc8999 100644 --- a/.bazelignore +++ b/.bazelignore @@ -2,3 +2,4 @@ pkg bin .git .idea +web diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..28f1a1789 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +bazel-* +bin +web/**/.next +web/**/coverage +web/**/dist +web/**/node_modules +web/**/playwright-report +web/**/test-results +web/**/*.tsbuildinfo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d37772938..e3aab7397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,24 @@ jobs: if: ${{ failure() }} uses: ./.github/actions/upload-testlogs + web: + name: Web + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: web/.nvmrc + - name: Enable Corepack + run: corepack enable + - name: Install web dependencies + run: make web-install + - name: Run web checks + run: make web-check + # --------------------------------------------------------------------------- # INTEGRATION TESTS (e2e, gateway, orchestrator) # --------------------------------------------------------------------------- @@ -126,6 +144,27 @@ jobs: if: ${{ failure() }} uses: ./.github/actions/upload-testlogs + web-e2e: + name: Web E2E + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: ./.github/actions/setup + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: web/.nvmrc + - name: Enable Corepack + run: corepack enable + - name: Install web dependencies + run: make web-install + - name: Install Playwright Chromium + run: corepack pnpm@10.17.1 --dir web exec playwright install --with-deps chromium + - name: Run real-stack web E2E + run: make web-e2e-test + gateway-integration-test: name: Gateway Integration Test if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} @@ -310,6 +349,8 @@ jobs: - queue-integration-test - storage-integration-test - consumer-integration-test + - web + - web-e2e - workflow-security steps: - name: Fail if any required check did not succeed diff --git a/.gitignore b/.gitignore index 08d5b9cc1..00326615e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,11 @@ bin/ # Make completion cache .make_targets_cache +/web/node_modules/ +/web/**/node_modules/ +/web/**/dist/ +/web/**/.next/ +/web/**/coverage/ +/web/**/playwright-report/ +/web/**/test-results/ +/web/**/*.tsbuildinfo diff --git a/Makefile b/Makefile index b15be5161..16877c486 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ COMPOSE = docker-compose # SubmitQueue compose files COMPOSE_FILE = service/submitqueue/docker-compose.yml +WEB_COMPOSE_FILE = service/submitqueue/docker-compose.web.yml GATEWAY_COMPOSE_FILE = service/submitqueue/gateway/server/docker-compose.yml ORCHESTRATOR_COMPOSE_FILE = service/submitqueue/orchestrator/server/docker-compose.yml @@ -41,6 +42,10 @@ PROTO_PACKAGES = api/base/change api/base/hook api/base/mergestrategy api/base/m # Set REPO_ROOT for docker-compose export REPO_ROOT := $(shell pwd) +# Local-only credential for the reference web host. Override it for manual +# testing; production authentication is deliberately outside this demo. +export SUBMITQUEUE_WEB_TOKEN ?= submitqueue-local-demo-token-change-me + # Which provider the demo stack targets, and the only difference between a free # local run and a live one. Selects a configuration directory rather than a code # path, so adding a provider is mostly adding a directory β€” see @@ -148,7 +153,7 @@ define assert_clean fi endef -.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto demo-requests deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache test-race tidy tidy-bazel tidy-go help +.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto demo-requests deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache test-race tidy tidy-bazel tidy-go web-build web-check web-e2e-test web-install web-proto help build: ## Build all services and examples @@ -361,7 +366,7 @@ local-submitqueue-clean: ## Stop the stack and remove its volumes, images, and P @echo "Cleaning all services and data..." @# The overlay is named so that volumes it declares β€” Runway's checkouts β€” @# are removed too, rather than surviving as an orphan. - @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local @rm -rf "$(SQ_GIT_SANDBOX_DIR)" @echo "All services, volumes, images, and $(SQ_GIT_SANDBOX_DIR) removed." @@ -442,7 +447,7 @@ local-runway-stop: ## Stop Runway service @echo "Runway services stopped." local-submitqueue-logs: ## View logs from all running services - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) logs -f + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) logs -f local-submitqueue-orchestrator-start: build-submitqueue-orchestrator-linux ## Start Orchestrator service locally (Orchestrator + 2 MySQL databases) @echo "Starting Orchestrator with docker-compose..." @@ -466,11 +471,12 @@ local-submitqueue-orchestrator-stop: ## Stop Orchestrator service local-submitqueue-ps: ## Show running containers and their ports @echo "Running containers and ports:" @echo "" - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) ps + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) ps @echo "" @echo "πŸ“‘ Service Endpoints:" @echo " Gateway gRPC: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'not running')" @echo " Orchestrator gRPC: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-orchestrator-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'not running')" + @echo " Web UX: http://localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-web-service-1 3000 2>/dev/null | cut -d: -f2 || echo 'not running')" @echo "" @echo "πŸ—„οΈ Database Endpoints:" @echo " MySQL App: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-mysql-app-1 3306 2>/dev/null | cut -d: -f2 || echo 'not running')" @@ -488,7 +494,7 @@ local-submitqueue-ps: ## Show running containers and their ports local-submitqueue-restart: build-all-linux ## Restart all services (rebuild and restart) @echo "Restarting all services..." - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) restart + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) restart @echo "Services restarted!" @make local-submitqueue-ps @@ -511,13 +517,16 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git| else \ export SQ_CONTAINER_USER=$$(id -u):$$(id -g); \ fi; \ - $(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) up -d --build --wait + $(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) up -d --build --wait @echo "Applying database schemas..." @$(MAKE) -s local-init-submitqueue-schemas @echo "" @echo "βœ… Stack is running against provider '$(PROVIDER)'." @echo "" @echo "Gateway gRPC port: $$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'unknown')" + @echo "Web UX: http://localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-web-service-1 3000 2>/dev/null | cut -d: -f2 || echo 'unknown')" + @echo "Web username: submitqueue" + @echo "Web token: $(SUBMITQUEUE_WEB_TOKEN)" @if [ "$(PROVIDER)" = "git" ]; then \ echo "Land target: $(SQ_GIT_SANDBOX_DIR)/sandbox.git"; \ fi @@ -527,7 +536,7 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git| local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sandbox; the databases do not survive) @echo "Stopping SubmitQueue services..." - @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down @# Both MySQL services mount anonymous volumes, so `down` detaches them and @# the next `up` creates fresh ones. Saying "data preserved" here would be @# read as "your requests are still there", which they are not. @@ -539,7 +548,7 @@ local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sand local-stop: ## Stop every local stack β€” SubmitQueue, Stovepipe, and Runway (keep data) @echo "Stopping all services..." - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down @$(COMPOSE) -f $(STOVEPIPE_COMPOSE_FILE) -p $(STOVEPIPE_LOCAL_PROJECT) down @$(COMPOSE) -f $(RUNWAY_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) down @echo "Services stopped. Data volumes preserved." @@ -644,6 +653,21 @@ tidy-go: ## Run go mod tidy @echo "Running go mod tidy..." @$(BAZEL) run @rules_go//go -- mod tidy -e +web-build: web-install ## Build all web packages and the Next.js host + @corepack pnpm@10.17.1 --dir web build + +web-check: ## Run web generation, packaging, lint, type, unit, and build checks + @corepack pnpm@10.17.1 --dir web check + +web-e2e-test: build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-runway-linux web-install ## Run the real-stack Playwright and axe web test + @web/tool/run-e2e.sh + +web-install: ## Install the pinned web workspace dependencies + @corepack pnpm@10.17.1 --dir web install --frozen-lockfile + +web-proto: web-install ## Regenerate the TypeScript protobuf API + @corepack pnpm@10.17.1 --dir web api:generate + help: ## Show this help message @echo "Available targets:" @echo "" diff --git a/doc/howto/QUICKSTART.md b/doc/howto/QUICKSTART.md index a094f8a0c..954fd5aba 100644 --- a/doc/howto/QUICKSTART.md +++ b/doc/howto/QUICKSTART.md @@ -30,10 +30,15 @@ Compose publishes each service on a **random** host port so several stacks can r βœ… Stack is running against provider 'fake'. Gateway gRPC port: 58537 +Web UX: http://localhost:58538 +Web username: submitqueue +Web token: submitqueue-local-demo-token-change-me ``` You do not have to note it down. Every command below finds the running stack's port for itself, which matters because Compose picks a fresh one on every start β€” a number copied from an earlier run is the most common reason a demo command cannot connect. Set `GATEWAY_ADDR=host:port` only to reach a gateway this Makefile did not start. +The Web UX is a read-only reference host for the same gateway. Open the printed URL and use the printed HTTP Basic credentials. It lists receipts from `demo-queue` in a fixed trailing 24-hour window; after `make demo-requests`, select any row to see its current status and retained lifecycle history. The host and its default credential are deliberately local-demo wiring, not a production authentication design. Override the token with `SUBMITQUEUE_WEB_TOKEN= make local-submitqueue-start` when desired. + ## Put traffic through it ```bash diff --git a/doc/rfc/index.md b/doc/rfc/index.md index 11dcb60d9..92f80c017 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -11,6 +11,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting - [Consumer Hold](consumer-hold.md) - Fourth delivery outcome letting a controller postpone its delivery: the message becomes a partition barrier that pauses consumption for a chosen delay, redelivers in order, and does not count as a failure toward dead-lettering - [Change URIs](change-uri.md) - Identity of a code change: `scheme://{host[:port]}/{path}` per provider (GitHub PR, Phabricator Diff, git ref/commit) and canonical-form rules - [Hooks Framework](hook-framework.md) - Fire-and-forget side effects off pipeline lifecycle events: one shared `HookEvent` contract (`api/base/hook/`) published to a durable per-domain hook topic, dispatched by a per-domain stage to a pluggable hook extension (`platform/extension/hook/`) for integrations like warehouse export and code-review notifications +- [Web as a Library](web-library.md) - Publishing gateway-backed components, presentation models, and server helpers as a reusable package while host-owned Next apps provide routes, authentication, telemetry, configuration, and gateway transport ## SubmitQueue diff --git a/doc/rfc/web-library.md b/doc/rfc/web-library.md new file mode 100644 index 000000000..23d1b7222 --- /dev/null +++ b/doc/rfc/web-library.md @@ -0,0 +1,63 @@ +# Web as a Library + +## Decision + +SubmitQueue publishes its browser UX as a library of components, gateway helpers, and presentation models. A deployer-owned Next application supplies routes, authentication, telemetry, configuration, and gateway connectivity. + +The package resembles `submitqueue/client/`, while the host resembles service wiring. The library consumes the published gateway contract and maps it to presentation. Domain extensions stay behind the gateway. + +Phase one is read-only: request summary, history, and queue receipt list ([status/list](submitqueue/status-list-api.md), [history](submitqueue/history-api.md)). Mutations require a separate authorization and audit design. + +## Host/library boundary + +| Library | Host | +|---|---| +| Gateway-to-presentation mapping, components, status and error display | Next application root, routes, and which subset to expose | +| Internal and external link helpers | Queue names, per-queue gateway routing, credentials, and deadlines | +| OpenTelemetry API instrumentation and a structural `Logger` | Session authorization, telemetry backends, configuration, and process lifecycle | + +The host configures the queues it serves and selects each generated client with `(queue) => client`. It builds those clients with `@connectrpc/connect-node` `createGrpcTransport` over HTTP/2: TLS by default, plaintext h2c only as an explicit local option. It installs the library's tracing interceptor while constructing the transport. The gateway stays reachable only by trusted hosts. + +Components render serializable props and do not fetch. Sessions, gateway clients, and protobuf messages stay on the server. Server helpers call `connection()` before gateway I/O, host routes that call them are dynamically rendered, and neither side caches gateway results. + +`WebPaths` selects internal links. Each substituted value is unpadded base64url, so a slash inside an sqid (`/`) remains one path segment after a proxy decodes `%2F`. External links require a host mapping keyed by change-URI scheme and authority; other values render as text. Build URLs stay on history occurrences, because one request can have concurrent sibling builds. + +`List` requires a queue and a half-open receipt window. A first visit uses a trailing 24-hour window, which the host may override. Both bounds live in the route's search parameters and stay fixed across refresh and paging, because the page token is valid only for that queue and those bounds. Changing the window drops the token. + +A client component calls `router.refresh()` to rerun the server loaders. One refresh runs at a time. The wait is the terminal client's poll interval plus jitter, grows after a transport failure, and pauses while the document is hidden or the browser is offline. A request view stops on `landed`, `error`, or `cancelled`. A queue list keeps polling for the life of its fixed window. + +## Package shape + +The pnpm workspace lives under `web/`, with a nested `web/go.mod` so the Go build graph does not index it: + +``` +web/ +β”œβ”€β”€ api/ # @submitqueue/api, one package for base and gateway stubs +β”œβ”€β”€ submitqueue/ # @submitqueue/web-submitqueue +└── service/submitqueue/ # reference host +``` + +Generated TypeScript is committed beside the Go stubs. Gateway stubs and the base protos they import share one package, so the imports stay valid after publish. The library ships ESM at the root, `./server`, and `./testing`. The root preserves `'use client'` and reaches no `server-only` or Node-only code. React, Next, Connect, and Protobuf-ES are peers. Shared UI waits until a second domain needs it. + +## Prerequisites + +- Typed gRPC details for known errors. Helpers classify `InvalidArgument`, `NotFound`, and `ResourceExhausted` as user errors, `Unavailable` and `DeadlineExceeded` as transient infrastructure errors, and every other code as an infrastructure error. Raw gateway messages are not rendered. +- A typed build URL on history occurrences. +- Status values stay strings. The library has a display table and an unknown fallback, checked against a fixture in `api/submitqueue/gateway/testdata/`. +- Protobuf `int64` timestamps become numeric milliseconds in presentation models after a safe-range check. + +## Acceptance + +- Go and Vitest tests cover proto drift, the status fixture, timestamp conversion, stable list bounds, base64url paths, trusted links, and error classification. +- Component tests cover rendering and polling controls: single-flight, jitter and backoff, hidden and offline pause, and terminal stop. +- Playwright and axe-core cover browser accessibility, responsive and visual states, navigation, and stable focus and scroll. +- Every host route rejects a missing or denied session without relying on `proxy.ts`. +- A Node-owned Compose check, outside Bazel and in required checks, runs the reference host against the real grpc-go gateway as `e2e-submitqueue-web` ([testing guide](../howto/TESTING.md#container-naming)). It covers TLS and explicit h2c, and a slash-containing sqid fetched through the deployment proxy. +- `pnpm pack` tarballs install into an external Next app, match the root, `./server`, and `./testing` export map, keep `server-only` out of the root, and build at `next >=16.3 <17` with both supported React 19 bounds. +- Browser fakes are a build-time alias. The production artifact contains none. + +## Rejected + +- **A repository-owned application or container.** Another deployer would have to fork routes, authentication, telemetry, and transport. +- **Static export or embedding in a Go binary.** Request routes are dynamic, and the gateway stays private to a server that can authorize the caller. +- **A generic runtime, DI container, or web-extension layer.** Next's filesystem and the generated gateway client are the composition boundaries. diff --git a/service/submitqueue/docker-compose.web.yml b/service/submitqueue/docker-compose.web.yml new file mode 100644 index 000000000..671517eca --- /dev/null +++ b/service/submitqueue/docker-compose.web.yml @@ -0,0 +1,34 @@ +# Compose overlay for the local, read-only SubmitQueue web demo. +# +# The browser talks only to this Next.js host. The host reaches the private +# grpc-go gateway over Compose's internal network using explicit plaintext +# HTTP/2, which is intentionally limited to the local demo. + +services: + web-service: + image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-web-service:latest + build: + context: ${REPO_ROOT} + dockerfile: web/service/submitqueue/Dockerfile + ports: + - "3000" + environment: + - PORT=3000 + - HOSTNAME=0.0.0.0 + - SUBMITQUEUE_GATEWAY_URL=http://gateway-service:8080 + - SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT=true + - SUBMITQUEUE_WEB_TOKEN=${SUBMITQUEUE_WEB_TOKEN:?set SUBMITQUEUE_WEB_TOKEN to at least 32 characters} + depends_on: + gateway-service: + condition: service_started + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))", + ] + interval: 2s + timeout: 2s + retries: 30 diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 000000000..442c7587a --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +22.20.0 diff --git a/web/api/package.json b/web/api/package.json new file mode 100644 index 000000000..02e08eee1 --- /dev/null +++ b/web/api/package.json @@ -0,0 +1,32 @@ +{ + "name": "@submitqueue/api", + "version": "0.1.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "exports": { + "./base/change": { + "types": "./dist/api/base/change/proto/change_pb.d.ts", + "import": "./dist/api/base/change/proto/change_pb.js" + }, + "./base/mergestrategy": { + "types": "./dist/api/base/mergestrategy/proto/mergestrategy_pb.d.ts", + "import": "./dist/api/base/mergestrategy/proto/mergestrategy_pb.js" + }, + "./submitqueue/gateway": { + "types": "./dist/api/submitqueue/gateway/proto/gateway_pb.d.ts", + "import": "./dist/api/submitqueue/gateway/proto/gateway_pb.js" + } + }, + "scripts": { + "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.build.json", + "lint": "tsc --project tsconfig.json", + "test": "vitest run", + "typecheck": "tsc --project tsconfig.json" + }, + "dependencies": { + "@bufbuild/protobuf": "2.15.0" + } +} diff --git a/web/api/src/api.test.ts b/web/api/src/api.test.ts new file mode 100644 index 000000000..74439b1d8 --- /dev/null +++ b/web/api/src/api.test.ts @@ -0,0 +1,23 @@ +import { create } from "@bufbuild/protobuf"; +import { describe, expect, it } from "vitest"; + +import { + ListRequestSchema, + SubmitQueueGateway, +} from "./gen/api/submitqueue/gateway/proto/gateway_pb.js"; + +describe("generated gateway API", () => { + it("constructs list requests without losing int64 values", () => { + const request = create(ListRequestSchema, { + queue: "demo-queue", + receivedAtOrAfterMs: 1_700_000_000_000n, + receivedBeforeMs: 1_700_086_400_000n, + pageSize: 50, + }); + + expect(request.queue).toBe("demo-queue"); + expect(request.receivedAtOrAfterMs).toBe(1_700_000_000_000n); + expect(request.receivedBeforeMs).toBe(1_700_086_400_000n); + expect(SubmitQueueGateway.methods.some((method) => method.localName === "list")).toBe(true); + }); +}); diff --git a/web/api/src/gen/api/base/change/proto/change_pb.ts b/web/api/src/gen/api/base/change/proto/change_pb.ts new file mode 100644 index 000000000..a2241fb04 --- /dev/null +++ b/web/api/src/gen/api/base/change/proto/change_pb.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.15.0 with parameter "target=ts,import_extension=js" +// @generated from file api/base/change/proto/change.proto (package uber.base.change, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file api/base/change/proto/change.proto. + */ +export const file_api_base_change_proto_change: GenFile = /*@__PURE__*/ + fileDesc("CiJhcGkvYmFzZS9jaGFuZ2UvcHJvdG8vY2hhbmdlLnByb3RvEhB1YmVyLmJhc2UuY2hhbmdlIhYKBkNoYW5nZRIMCgR1cmlzGAEgAygJQmYKIGNvbS51YmVyLnN1Ym1pdHF1ZXVlLmJhc2UuY2hhbmdlQgtDaGFuZ2VQcm90b1ABWjNnaXRodWIuY29tL3ViZXIvc3VibWl0cXVldWUvYXBpL2Jhc2UvY2hhbmdlL3Byb3RvcGJiBnByb3RvMw"); + +/** + * Change represents a code change identified by URIs from a code change provider + * (e.g. a GitHub Pull Request, a Phabricator Diff, or a git commit). The provider + * is extracted from the URI scheme. + * + * This is the shared wire contract for change identity, reused across SubmitQueue, + * Stovepipe, and other repo-local domains β€” the proto-level analog of the + * platform/base/change Go entity. Domains import it rather than redefining their own. + * + * @generated from message uber.base.change.Change + */ +export type Change = Message<"uber.base.change.Change"> & { + /** + * URIs identifying the change(s) (RFC 3986 compliant): scheme:///. + * The scheme identifies the change provider, the authority is the provider instance + * the change lives on, and the path contains provider-specific resource identifiers. + * + * Supported by default (other providers can be added): + * GitHub PR: "github://///pull//" + * Phabricator Diff: "phab:///D/" + * git commit: "git://///" + * ( is a fully-qualified, percent-encoded git ref) + * + * The commit SHA must be the full 40-character lowercase hex SHA; abbreviated + * SHAs are rejected because downstream staleness checks compare by strict equality. + * + * One URI is one unit of change -- a single pull request, revision, or + * commit -- and it is the granularity everything downstream operates at. A + * list is therefore an ordered set of distinct changes (a stack), not one + * change described several ways: they are applied in the order given, each + * on top of the last, and each yields its own result. An integration + * strategy chosen for a change applies to every URI in the list, the same + * way to each. + * + * @generated from field: repeated string uris = 1; + */ + uris: string[]; +}; + +/** + * Describes the message uber.base.change.Change. + * Use `create(ChangeSchema)` to create a new message. + */ +export const ChangeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_base_change_proto_change, 0); + diff --git a/web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts b/web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts new file mode 100644 index 000000000..84c45fc23 --- /dev/null +++ b/web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.15.0 with parameter "target=ts,import_extension=js" +// @generated from file api/base/mergestrategy/proto/mergestrategy.proto (package uber.base.mergestrategy, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc } from "@bufbuild/protobuf/codegenv2"; + +/** + * Describes the file api/base/mergestrategy/proto/mergestrategy.proto. + */ +export const file_api_base_mergestrategy_proto_mergestrategy: GenFile = /*@__PURE__*/ + fileDesc("CjBhcGkvYmFzZS9tZXJnZXN0cmF0ZWd5L3Byb3RvL21lcmdlc3RyYXRlZ3kucHJvdG8SF3ViZXIuYmFzZS5tZXJnZXN0cmF0ZWd5Kk4KCFN0cmF0ZWd5EgsKB0RFRkFVTFQQABIKCgZSRUJBU0UQARIRCg1TUVVBU0hfUkVCQVNFEAISCQoFTUVSR0UQAxILCgdQUk9NT1RFEARCewonY29tLnViZXIuc3VibWl0cXVldWUuYmFzZS5tZXJnZXN0cmF0ZWd5QhJNZXJnZVN0cmF0ZWd5UHJvdG9QAVo6Z2l0aHViLmNvbS91YmVyL3N1Ym1pdHF1ZXVlL2FwaS9iYXNlL21lcmdlc3RyYXRlZ3kvcHJvdG9wYmIGcHJvdG8z"); + +/** + * Strategy defines the possible source control integration methods β€” how a change + * is integrated into the target branch. + * + * This is the shared wire contract for merge strategy, reused across SubmitQueue + * and other repo-local domains β€” the proto-level analog of the + * platform/base/mergestrategy Go entity. Domains import it rather than redefining their own. + * + * A strategy is chosen once for a change and applies to every URI that change + * carries, the same way to each. Since one URI is one unit of change (see + * uber.base.change.Change), the values below are defined per URI: a change + * carrying three URIs is integrated as three separate applications of the same + * strategy, in order, not as one combined application. + * + * @generated from enum uber.base.mergestrategy.Strategy + */ +export enum Strategy { + /** + * Default strategy (let the server decide based on configuration). + * + * @generated from enum value: DEFAULT = 0; + */ + DEFAULT = 0, + + /** + * Rebase the commits the change introduces onto the target branch before landing. + * + * @generated from enum value: REBASE = 1; + */ + REBASE = 1, + + /** + * Same as REBASE, then squash into a single commit. The squash unit is the + * individual change: a change of ten commits becomes one commit, and a + * stack of three URIs becomes three commits rather than one, so the + * boundary between the stacked changes survives. + * + * @generated from enum value: SQUASH_REBASE = 2; + */ + SQUASH_REBASE = 2, + + /** + * Merge into the target branch by creating a separate merge commit, preserving commit history along with hashes. + * + * @generated from enum value: MERGE = 3; + */ + MERGE = 3, + + /** + * Integrate the exact revision as-is, with no content transform β€” advance the target branch to an already-existing + * commit rather than producing new revisions. The implementer maps it to its backend: git fast-forward, Mercurial + * bookmark advance, Subversion/Perforce copy. Used to promote an already-landed/verified commit onto another branch. + * + * The one strategy that constrains the list rather than repeating over it: + * advancing a ref to an exact revision admits a single URI, because a + * second one could not also be the revision the target ends at. + * + * @generated from enum value: PROMOTE = 4; + */ + PROMOTE = 4, +} + +/** + * Describes the enum uber.base.mergestrategy.Strategy. + */ +export const StrategySchema: GenEnum = /*@__PURE__*/ + enumDesc(file_api_base_mergestrategy_proto_mergestrategy, 0); + diff --git a/web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts b/web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts new file mode 100644 index 000000000..96351a112 --- /dev/null +++ b/web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts @@ -0,0 +1,822 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.15.0 with parameter "target=ts,import_extension=js" +// @generated from file api/submitqueue/gateway/proto/gateway.proto (package uber.submitqueue.gateway, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Change } from "../../../base/change/proto/change_pb.js"; +import { file_api_base_change_proto_change } from "../../../base/change/proto/change_pb.js"; +import type { Strategy } from "../../../base/mergestrategy/proto/mergestrategy_pb.js"; +import { file_api_base_mergestrategy_proto_mergestrategy } from "../../../base/mergestrategy/proto/mergestrategy_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file api/submitqueue/gateway/proto/gateway.proto. + */ +export const file_api_submitqueue_gateway_proto_gateway: GenFile = /*@__PURE__*/ + fileDesc("CithcGkvc3VibWl0cXVldWUvZ2F0ZXdheS9wcm90by9nYXRld2F5LnByb3RvEhh1YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkiHgoLUGluZ1JlcXVlc3QSDwoHbWVzc2FnZRgBIAEoCSJaCgxQaW5nUmVzcG9uc2USDwoHbWVzc2FnZRgBIAEoCRIUCgxzZXJ2aWNlX25hbWUYAiABKAkSEQoJdGltZXN0YW1wGAMgASgDEhAKCGhvc3RuYW1lGAQgASgJInsKC0xhbmRSZXF1ZXN0Eg0KBXF1ZXVlGAEgASgJEigKBmNoYW5nZRgCIAEoCzIYLnViZXIuYmFzZS5jaGFuZ2UuQ2hhbmdlEjMKCHN0cmF0ZWd5GAQgASgOMiEudWJlci5iYXNlLm1lcmdlc3RyYXRlZ3kuU3RyYXRlZ3kiHAoMTGFuZFJlc3BvbnNlEgwKBHNxaWQYASABKAkiPAoNQ2FuY2VsUmVxdWVzdBIMCgRzcWlkGAEgASgJEg4KBnJlYXNvbhgCIAEoCRINCgVxdWV1ZRgDIAEoCSIQCg5DYW5jZWxSZXNwb25zZSL5AQoOUmVxdWVzdFN1bW1hcnkSDAoEc3FpZBgBIAEoCRINCgVxdWV1ZRgCIAEoCRITCgtjaGFuZ2VfdXJpcxgDIAMoCRIWCg5yZWNlaXZlZF9hdF9tcxgEIAEoAxIOCgZzdGF0dXMYBSABKAkSEgoKbGFzdF9lcnJvchgGIAEoCRJICghtZXRhZGF0YRgHIAMoCzI2LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeS5NZXRhZGF0YUVudHJ5Gi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7ChxHZXRSZXF1ZXN0U3VtbWFyeUJ5SURSZXF1ZXN0EgwKBHNxaWQYASABKAkSDQoFcXVldWUYAiABKAkiWgodR2V0UmVxdWVzdFN1bW1hcnlCeUlEUmVzcG9uc2USOQoHcmVxdWVzdBgBIAEoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeSJICiNHZXRSZXF1ZXN0U3VtbWFyeUJ5Q2hhbmdlVVJJUmVxdWVzdBISCgpjaGFuZ2VfdXJpGAEgASgJEg0KBXF1ZXVlGAIgASgJImIKJEdldFJlcXVlc3RTdW1tYXJ5QnlDaGFuZ2VVUklSZXNwb25zZRI6CghyZXF1ZXN0cxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeSKAAQoLTGlzdFJlcXVlc3QSDQoFcXVldWUYASABKAkSHwoXcmVjZWl2ZWRfYXRfb3JfYWZ0ZXJfbXMYAiABKAMSGgoScmVjZWl2ZWRfYmVmb3JlX21zGAMgASgDEhEKCXBhZ2Vfc2l6ZRgEIAEoBRISCgpwYWdlX3Rva2VuGAUgASgJImMKDExpc3RSZXNwb25zZRI6CghyZXF1ZXN0cxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkiOwocR2V0UmVxdWVzdEhpc3RvcnlCeUlEUmVxdWVzdBIMCgRzcWlkGAEgASgJEg0KBXF1ZXVlGAIgASgJIt4BCgxIaXN0b3J5RXZlbnQSFAoMdGltZXN0YW1wX21zGAEgASgDEg4KBnN0YXR1cxgCIAEoCRISCgpsYXN0X2Vycm9yGAMgASgJEkYKCG1ldGFkYXRhGAQgAygLMjQudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5Lkhpc3RvcnlFdmVudC5NZXRhZGF0YUVudHJ5EgwKBHR5cGUYBSABKAkSDQoFZXZlbnQYBiABKAkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlcKHUdldFJlcXVlc3RIaXN0b3J5QnlJRFJlc3BvbnNlEjYKBmV2ZW50cxgBIAMoCzImLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5IaXN0b3J5RXZlbnQiSAojR2V0UmVxdWVzdEhpc3RvcnlCeUNoYW5nZVVSSVJlcXVlc3QSEgoKY2hhbmdlX3VyaRgBIAEoCRINCgVxdWV1ZRgCIAEoCSJWCg5SZXF1ZXN0SGlzdG9yeRIMCgRzcWlkGAEgASgJEjYKBmV2ZW50cxgCIAMoCzImLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5IaXN0b3J5RXZlbnQiYwokR2V0UmVxdWVzdEhpc3RvcnlCeUNoYW5nZVVSSVJlc3BvbnNlEjsKCWhpc3RvcmllcxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0SGlzdG9yeSIYCgVFcnJvchIPCgdtZXNzYWdlGAEgASgJIlcKFlVucmVjb2duaXplZFF1ZXVlRXJyb3ISLgoFZXJyb3IYASABKAsyHy51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuRXJyb3ISDQoFcXVldWUYAiABKAkiaAoUUmVxdWVzdE5vdEZvdW5kRXJyb3ISLgoFZXJyb3IYASABKAsyHy51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuRXJyb3ISDAoEc3FpZBgCIAEoCRISCgpjaGFuZ2VfdXJpGAMgASgJMtwHChJTdWJtaXRRdWV1ZUdhdGV3YXkSVwoEUGluZxIlLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5QaW5nUmVxdWVzdBomLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5QaW5nUmVzcG9uc2UiABJXCgRMYW5kEiUudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkxhbmRSZXF1ZXN0GiYudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkxhbmRSZXNwb25zZSIAEl0KBkNhbmNlbBInLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5DYW5jZWxSZXF1ZXN0GigudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkNhbmNlbFJlc3BvbnNlIgASigEKFUdldFJlcXVlc3RTdW1tYXJ5QnlJRBI2LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5HZXRSZXF1ZXN0U3VtbWFyeUJ5SURSZXF1ZXN0GjcudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkdldFJlcXVlc3RTdW1tYXJ5QnlJRFJlc3BvbnNlIgASnwEKHEdldFJlcXVlc3RTdW1tYXJ5QnlDaGFuZ2VVUkkSPS51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuR2V0UmVxdWVzdFN1bW1hcnlCeUNoYW5nZVVSSVJlcXVlc3QaPi51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuR2V0UmVxdWVzdFN1bW1hcnlCeUNoYW5nZVVSSVJlc3BvbnNlIgASVwoETGlzdBIlLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5MaXN0UmVxdWVzdBomLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5MaXN0UmVzcG9uc2UiABKKAQoVR2V0UmVxdWVzdEhpc3RvcnlCeUlEEjYudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkdldFJlcXVlc3RIaXN0b3J5QnlJRFJlcXVlc3QaNy51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuR2V0UmVxdWVzdEhpc3RvcnlCeUlEUmVzcG9uc2UiABKfAQocR2V0UmVxdWVzdEhpc3RvcnlCeUNoYW5nZVVSSRI9LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5HZXRSZXF1ZXN0SGlzdG9yeUJ5Q2hhbmdlVVJJUmVxdWVzdBo+LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5HZXRSZXF1ZXN0SGlzdG9yeUJ5Q2hhbmdlVVJJUmVzcG9uc2UiAEJrChxjb20udWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5QgxHYXRld2F5UHJvdG9QAVo7Z2l0aHViLmNvbS91YmVyL3N1Ym1pdHF1ZXVlL2FwaS9zdWJtaXRxdWV1ZS9nYXRld2F5L3Byb3RvcGJiBnByb3RvMw", [file_api_base_change_proto_change, file_api_base_mergestrategy_proto_mergestrategy]); + +/** + * PingRequest is the request for the Ping method + * + * @generated from message uber.submitqueue.gateway.PingRequest + */ +export type PingRequest = Message<"uber.submitqueue.gateway.PingRequest"> & { + /** + * Optional message to include in the ping + * + * @generated from field: string message = 1; + */ + message: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.PingRequest. + * Use `create(PingRequestSchema)` to create a new message. + */ +export const PingRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 0); + +/** + * PingResponse is the response for the Ping method + * + * @generated from message uber.submitqueue.gateway.PingResponse + */ +export type PingResponse = Message<"uber.submitqueue.gateway.PingResponse"> & { + /** + * The response message + * + * @generated from field: string message = 1; + */ + message: string; + + /** + * The service name that handled the request + * + * @generated from field: string service_name = 2; + */ + serviceName: string; + + /** + * Timestamp of when the ping was received + * + * @generated from field: int64 timestamp = 3; + */ + timestamp: bigint; + + /** + * Hostname of the server that handled the request + * + * @generated from field: string hostname = 4; + */ + hostname: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.PingResponse. + * Use `create(PingResponseSchema)` to create a new message. + */ +export const PingResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 1); + +/** + * LandRequest defines a request to land a set of code changes on the source control repository's target branch. + * + * SubmitQueue guarantees changes are landed in order with no other changes in between. + * SubmitQueue does not guarantee each change is individually valid, but produces a validity marker on such changes. + * The user is responsible for including all changes in a stack in order, starting from the earliest change. + * + * @generated from message uber.submitqueue.gateway.LandRequest + */ +export type LandRequest = Message<"uber.submitqueue.gateway.LandRequest"> & { + /** + * Name of the queue processing the land request. The queue should be defined in the configuration, otherwise an error is returned. + * + * @generated from field: string queue = 1; + */ + queue: string; + + /** + * Change (such as a pull request) to land into the target branch. Target branch is defined by the queue configuration. + * + * @generated from field: uber.base.change.Change change = 2; + */ + change?: Change | undefined; + + /** + * Source control integration strategy to use for this land operation. If not specified, the default queue strategy is used. + * It applies to every URI the change carries, the same way to each β€” a land + * request cannot pick a different strategy per URI. + * + * @generated from field: uber.base.mergestrategy.Strategy strategy = 4; + */ + strategy: Strategy; +}; + +/** + * Describes the message uber.submitqueue.gateway.LandRequest. + * Use `create(LandRequestSchema)` to create a new message. + */ +export const LandRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 2); + +/** + * LandResponse defines the response to a land request. + * + * @generated from message uber.submitqueue.gateway.LandResponse + */ +export type LandResponse = Message<"uber.submitqueue.gateway.LandResponse"> & { + /** + * Globally unique identifier for the land request. Used to track the land request lifecycle. + * + * @generated from field: string sqid = 1; + */ + sqid: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.LandResponse. + * Use `create(LandResponseSchema)` to create a new message. + */ +export const LandResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 3); + +/** + * CancelRequest defines a request to cancel an in-flight land request. If the request is part of a batch, the entire batch is cancelled. + * + * @generated from message uber.submitqueue.gateway.CancelRequest + */ +export type CancelRequest = Message<"uber.submitqueue.gateway.CancelRequest"> & { + /** + * Globally unique identifier of the land request to cancel, returned by a prior Land call. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Optional human-readable reason for the cancellation. Recorded for observability. + * + * @generated from field: string reason = 2; + */ + reason: string; + + /** + * Name of the queue processing the request. Required. A sqid is only resolvable within its own queue, + * so naming a queue the request does not belong to is reported as not found. + * + * @generated from field: string queue = 3; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.CancelRequest. + * Use `create(CancelRequestSchema)` to create a new message. + */ +export const CancelRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 4); + +/** + * CancelResponse defines the response to a cancel request. Empty on success. + * + * A successful response indicates only that the cancellation intent was accepted and enqueued β€” it does NOT confirm + * that the request was actually cancelled. The terminal outcome may still be "landed" or "error" if a race was lost. + * Errors are returned as standard status codes (e.g. InvalidArgument for an empty sqid). + * + * @generated from message uber.submitqueue.gateway.CancelResponse + */ +export type CancelResponse = Message<"uber.submitqueue.gateway.CancelResponse"> & { +}; + +/** + * Describes the message uber.submitqueue.gateway.CancelResponse. + * Use `create(CancelResponseSchema)` to create a new message. + */ +export const CancelResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 5); + +/** + * RequestSummary is the gateway-owned materialized current view of one received request. + * + * @generated from message uber.submitqueue.gateway.RequestSummary + */ +export type RequestSummary = Message<"uber.submitqueue.gateway.RequestSummary"> & { + /** + * Globally unique request identifier. This is request_id in internal storage. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Queue supplied when the request was received. + * + * @generated from field: string queue = 2; + */ + queue: string; + + /** + * Change URIs supplied when the request was received, in caller order. + * + * @generated from field: repeated string change_uris = 3; + */ + changeUris: string[]; + + /** + * Receipt time in Unix milliseconds. + * + * @generated from field: int64 received_at_ms = 4; + */ + receivedAtMs: bigint; + + /** + * Current customer-friendly status of the request. + * + * @generated from field: string status = 5; + */ + status: string; + + /** + * Last error associated with the current status. Empty when absent. + * + * @generated from field: string last_error = 6; + */ + lastError: string; + + /** + * Display and debugging metadata associated with the current status. + * + * @generated from field: map metadata = 7; + */ + metadata: { [key: string]: string }; +}; + +/** + * Describes the message uber.submitqueue.gateway.RequestSummary. + * Use `create(RequestSummarySchema)` to create a new message. + */ +export const RequestSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 6); + +/** + * GetRequestSummaryByIDRequest selects one request by the sqid returned from Land. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByIDRequest + */ +export type GetRequestSummaryByIDRequest = Message<"uber.submitqueue.gateway.GetRequestSummaryByIDRequest"> & { + /** + * Globally unique identifier for the request. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Name of the queue processing the request. Required. A sqid is only resolvable within its own queue, + * so naming a queue the request does not belong to is reported as not found. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByIDRequest. + * Use `create(GetRequestSummaryByIDRequestSchema)` to create a new message. + */ +export const GetRequestSummaryByIDRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 7); + +/** + * GetRequestSummaryByIDResponse contains the current materialized request view. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByIDResponse + */ +export type GetRequestSummaryByIDResponse = Message<"uber.submitqueue.gateway.GetRequestSummaryByIDResponse"> & { + /** + * Matching request. + * + * @generated from field: uber.submitqueue.gateway.RequestSummary request = 1; + */ + request?: RequestSummary | undefined; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByIDResponse. + * Use `create(GetRequestSummaryByIDResponseSchema)` to create a new message. + */ +export const GetRequestSummaryByIDResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 8); + +/** + * GetRequestSummaryByChangeURIRequest selects requests by an exact pinned change URI. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByChangeURIRequest + */ +export type GetRequestSummaryByChangeURIRequest = Message<"uber.submitqueue.gateway.GetRequestSummaryByChangeURIRequest"> & { + /** + * Exact change URI supplied in a Land request. + * + * @generated from field: string change_uri = 1; + */ + changeUri: string; + + /** + * Name of the queue to search. Required. Results are scoped to this queue: a change URI landed + * into several queues is looked up one queue at a time. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByChangeURIRequest. + * Use `create(GetRequestSummaryByChangeURIRequestSchema)` to create a new message. + */ +export const GetRequestSummaryByChangeURIRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 9); + +/** + * GetRequestSummaryByChangeURIResponse contains matching requests newest first. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse + */ +export type GetRequestSummaryByChangeURIResponse = Message<"uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse"> & { + /** + * Matching requests ordered by receipt time descending, then sqid descending. + * + * @generated from field: repeated uber.submitqueue.gateway.RequestSummary requests = 1; + */ + requests: RequestSummary[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse. + * Use `create(GetRequestSummaryByChangeURIResponseSchema)` to create a new message. + */ +export const GetRequestSummaryByChangeURIResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 10); + +/** + * ListRequest defines one bounded queue receipt-history query. + * + * @generated from message uber.submitqueue.gateway.ListRequest + */ +export type ListRequest = Message<"uber.submitqueue.gateway.ListRequest"> & { + /** + * Exact queue to query. + * + * @generated from field: string queue = 1; + */ + queue: string; + + /** + * Inclusive lower receipt-time bound in Unix milliseconds. + * + * @generated from field: int64 received_at_or_after_ms = 2; + */ + receivedAtOrAfterMs: bigint; + + /** + * Exclusive upper receipt-time bound in Unix milliseconds. + * + * @generated from field: int64 received_before_ms = 3; + */ + receivedBeforeMs: bigint; + + /** + * Maximum results to return. Zero selects the server default; values above the server maximum are invalid. + * + * @generated from field: int32 page_size = 4; + */ + pageSize: number; + + /** + * Opaque continuation token from a previous response. Empty selects the first page. + * + * @generated from field: string page_token = 5; + */ + pageToken: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.ListRequest. + * Use `create(ListRequestSchema)` to create a new message. + */ +export const ListRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 11); + +/** + * ListResponse defines one page of queue receipt history. + * + * @generated from message uber.submitqueue.gateway.ListResponse + */ +export type ListResponse = Message<"uber.submitqueue.gateway.ListResponse"> & { + /** + * Requests ordered by receipt time descending, then sqid descending. + * + * @generated from field: repeated uber.submitqueue.gateway.RequestSummary requests = 1; + */ + requests: RequestSummary[]; + + /** + * Opaque continuation token. Empty when this is the last page. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.ListResponse. + * Use `create(ListResponseSchema)` to create a new message. + */ +export const ListResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 12); + +/** + * GetRequestHistoryByIDRequest selects one retained request history by sqid. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByIDRequest + */ +export type GetRequestHistoryByIDRequest = Message<"uber.submitqueue.gateway.GetRequestHistoryByIDRequest"> & { + /** + * Globally unique identifier for the request. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Name of the queue processing the request. Required. A sqid is only resolvable within its own queue, + * so naming a queue the request does not belong to is reported as not found. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByIDRequest. + * Use `create(GetRequestHistoryByIDRequestSchema)` to create a new message. + */ +export const GetRequestHistoryByIDRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 13); + +/** + * HistoryEvent is one retained append-only request-log event. + * + * @generated from message uber.submitqueue.gateway.HistoryEvent + */ +export type HistoryEvent = Message<"uber.submitqueue.gateway.HistoryEvent"> & { + /** + * Time the request-log entry was created, in Unix milliseconds. + * + * @generated from field: int64 timestamp_ms = 1; + */ + timestampMs: bigint; + + /** + * Customer-friendly request status recorded by the event. Set only when type is "status". + * + * @generated from field: string status = 2; + */ + status: string; + + /** + * Error message associated with the event. Empty when absent. + * + * @generated from field: string last_error = 3; + */ + lastError: string; + + /** + * Display and debugging metadata associated with this event. Each lifecycle event carries its own values. + * + * @generated from field: map metadata = 4; + */ + metadata: { [key: string]: string }; + + /** + * What this entry records: "status" when the request reached a position in the pipeline, + * "event" when something happened while it sat at one. Exactly one of status and event is set. + * + * @generated from field: string type = 5; + */ + type: string; + + /** + * Occurrence recorded by the entry, e.g. a build starting or finishing. Set only when type is "event". + * A request records many of these β€” one per build β€” and they never change its current status. + * + * @generated from field: string event = 6; + */ + event: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.HistoryEvent. + * Use `create(HistoryEventSchema)` to create a new message. + */ +export const HistoryEventSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 14); + +/** + * GetRequestHistoryByIDResponse contains all retained events for one request. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByIDResponse + */ +export type GetRequestHistoryByIDResponse = Message<"uber.submitqueue.gateway.GetRequestHistoryByIDResponse"> & { + /** + * Events ordered by timestamp ascending with a stable storage tie-breaker. + * + * @generated from field: repeated uber.submitqueue.gateway.HistoryEvent events = 1; + */ + events: HistoryEvent[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByIDResponse. + * Use `create(GetRequestHistoryByIDResponseSchema)` to create a new message. + */ +export const GetRequestHistoryByIDResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 15); + +/** + * GetRequestHistoryByChangeURIRequest selects retained histories by an exact pinned change URI. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByChangeURIRequest + */ +export type GetRequestHistoryByChangeURIRequest = Message<"uber.submitqueue.gateway.GetRequestHistoryByChangeURIRequest"> & { + /** + * Exact change URI supplied in a Land request. + * + * @generated from field: string change_uri = 1; + */ + changeUri: string; + + /** + * Name of the queue to search. Required. Results are scoped to this queue: a change URI landed + * into several queues is looked up one queue at a time. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByChangeURIRequest. + * Use `create(GetRequestHistoryByChangeURIRequestSchema)` to create a new message. + */ +export const GetRequestHistoryByChangeURIRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 16); + +/** + * RequestHistory groups retained events for one request. + * + * @generated from message uber.submitqueue.gateway.RequestHistory + */ +export type RequestHistory = Message<"uber.submitqueue.gateway.RequestHistory"> & { + /** + * Globally unique request identifier. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Retained events ordered chronologically. + * + * @generated from field: repeated uber.submitqueue.gateway.HistoryEvent events = 2; + */ + events: HistoryEvent[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.RequestHistory. + * Use `create(RequestHistorySchema)` to create a new message. + */ +export const RequestHistorySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 17); + +/** + * GetRequestHistoryByChangeURIResponse contains retained histories ordered by numeric sqid counter ascending. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByChangeURIResponse + */ +export type GetRequestHistoryByChangeURIResponse = Message<"uber.submitqueue.gateway.GetRequestHistoryByChangeURIResponse"> & { + /** + * Matching request histories. + * + * @generated from field: repeated uber.submitqueue.gateway.RequestHistory histories = 1; + */ + histories: RequestHistory[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByChangeURIResponse. + * Use `create(GetRequestHistoryByChangeURIResponseSchema)` to create a new message. + */ +export const GetRequestHistoryByChangeURIResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 18); + +/** + * Generic error with metadata. Each custom error type should extend this message. + * + * @generated from message uber.submitqueue.gateway.Error + */ +export type Error = Message<"uber.submitqueue.gateway.Error"> & { + /** + * Free text error message describing the error. + * + * @generated from field: string message = 1; + */ + message: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.Error. + * Use `create(ErrorSchema)` to create a new message. + */ +export const ErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 19); + +/** + * UnrecognizedQueueError is returned when a queue name is not recognized. Typically this indicates a typo in the queue name or server misconfiguration. + * + * @generated from message uber.submitqueue.gateway.UnrecognizedQueueError + */ +export type UnrecognizedQueueError = Message<"uber.submitqueue.gateway.UnrecognizedQueueError"> & { + /** + * Free text error message describing the error. + * + * @generated from field: uber.submitqueue.gateway.Error error = 1; + */ + error?: Error | undefined; + + /** + * Name of the queue that was not recognized. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.UnrecognizedQueueError. + * Use `create(UnrecognizedQueueErrorSchema)` to create a new message. + */ +export const UnrecognizedQueueErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 20); + +/** + * RequestNotFoundError is returned when no request is found for the given sqid. Typically this indicates a typo in the sqid or that the request has not been accepted yet. + * + * @generated from message uber.submitqueue.gateway.RequestNotFoundError + */ +export type RequestNotFoundError = Message<"uber.submitqueue.gateway.RequestNotFoundError"> & { + /** + * Free text error message describing the error. + * + * @generated from field: uber.submitqueue.gateway.Error error = 1; + */ + error?: Error | undefined; + + /** + * The sqid that was not found. + * + * @generated from field: string sqid = 2; + */ + sqid: string; + + /** + * Exact change URI that was not found. Populated only for change-URI lookup. + * + * @generated from field: string change_uri = 3; + */ + changeUri: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.RequestNotFoundError. + * Use `create(RequestNotFoundErrorSchema)` to create a new message. + */ +export const RequestNotFoundErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 21); + +/** + * SubmitQueueGateway provides the gateway API + * + * @generated from service uber.submitqueue.gateway.SubmitQueueGateway + */ +export const SubmitQueueGateway: GenService<{ + /** + * Ping returns a response indicating the service is alive + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.Ping + */ + ping: { + methodKind: "unary"; + input: typeof PingRequestSchema; + output: typeof PingResponseSchema; + }, + /** + * Land lands a set of code changes into a target branch, performing the necessary validations across all other changes in the queue. + * The processing is asynchronous and returns a LandResponse immediately. The land request is processed in the background. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.Land + */ + land: { + methodKind: "unary"; + input: typeof LandRequestSchema; + output: typeof LandResponseSchema; + }, + /** + * Cancel cancels an in-flight land request identified by its sqid. If the request has already been included in a batch, + * the entire batch is cancelled. Cancel is idempotent β€” calling it for an already-terminal request is a no-op. + * + * Cancellation is ASYNCHRONOUS: Cancel returns as soon as the cancellation intent has been accepted; the actual + * state transition is performed in the background by the orchestrator and may not have completed by the time the + * caller receives a response. + * + * Cancellation is NOT GUARANTEED: a request that has already landed, or that races to completion before the cancel + * signal propagates through the pipeline, may still land (or end in an error). Callers must NOT assume that a + * successful Cancel response means the request was cancelled β€” the actual terminal outcome (cancelled, landed, or + * error) must be checked through the request-summary or request-history APIs. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.Cancel + */ + cancel: { + methodKind: "unary"; + input: typeof CancelRequestSchema; + output: typeof CancelResponseSchema; + }, + /** + * GetRequestSummaryByID returns the current materialized status of one request. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestSummaryByID + */ + getRequestSummaryByID: { + methodKind: "unary"; + input: typeof GetRequestSummaryByIDRequestSchema; + output: typeof GetRequestSummaryByIDResponseSchema; + }, + /** + * GetRequestSummaryByChangeURI returns current materialized statuses for an exact pinned change URI. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestSummaryByChangeURI + */ + getRequestSummaryByChangeURI: { + methodKind: "unary"; + input: typeof GetRequestSummaryByChangeURIRequestSchema; + output: typeof GetRequestSummaryByChangeURIResponseSchema; + }, + /** + * List returns requests received for one queue during a bounded receipt-time range. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.List + */ + list: { + methodKind: "unary"; + input: typeof ListRequestSchema; + output: typeof ListResponseSchema; + }, + /** + * GetRequestHistoryByID returns every retained request-log event for one request. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestHistoryByID + */ + getRequestHistoryByID: { + methodKind: "unary"; + input: typeof GetRequestHistoryByIDRequestSchema; + output: typeof GetRequestHistoryByIDResponseSchema; + }, + /** + * GetRequestHistoryByChangeURI returns retained request histories for an exact pinned change URI. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestHistoryByChangeURI + */ + getRequestHistoryByChangeURI: { + methodKind: "unary"; + input: typeof GetRequestHistoryByChangeURIRequestSchema; + output: typeof GetRequestHistoryByChangeURIResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_api_submitqueue_gateway_proto_gateway, 0); + diff --git a/web/api/tsconfig.build.json b/web/api/tsconfig.build.json new file mode 100644 index 000000000..6ec5be4c9 --- /dev/null +++ b/web/api/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "src/gen", + "sourceMap": true + }, + "exclude": ["src/**/*.test.ts"] +} diff --git a/web/api/tsconfig.json b/web/api/tsconfig.json new file mode 100644 index 000000000..d26062d30 --- /dev/null +++ b/web/api/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/web/buf.gen.yaml b/web/buf.gen.yaml new file mode 100644 index 000000000..bbfd7a12e --- /dev/null +++ b/web/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v2 +clean: true +plugins: + - local: protoc-gen-es + out: api/src/gen + opt: + - target=ts + - import_extension=js diff --git a/web/e2e/submitqueue.spec.ts b/web/e2e/submitqueue.spec.ts new file mode 100644 index 000000000..fa7fca75d --- /dev/null +++ b/web/e2e/submitqueue.spec.ts @@ -0,0 +1,103 @@ +import AxeBuilder from "@axe-core/playwright"; +import { createClient } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { expect, request as playwrightRequest, test } from "@playwright/test"; + +import { Strategy } from "../api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.js"; +import { SubmitQueueGateway } from "../api/src/gen/api/submitqueue/gateway/proto/gateway_pb.js"; + +const queue = "demo-queue"; +const gatewayURL = process.env.SUBMITQUEUE_E2E_GATEWAY_URL as string; +const webURL = process.env.SUBMITQUEUE_E2E_WEB_URL as string; + +const gateway = createClient( + SubmitQueueGateway, + createGrpcTransport({ baseUrl: gatewayURL }), +); + +async function submitRequest(uri: string): Promise { + const response = await gateway.land({ + queue, + change: { uris: [uri] }, + strategy: Strategy.SQUASH_REBASE, + }); + expect(response.sqid).not.toBe(""); + await expect + .poll( + async () => { + const summary = await gateway.getRequestSummaryByID({ + queue, + sqid: response.sqid, + }); + return summary.request?.status; + }, + { timeout: 0 }, + ) + .toBe("landed"); + return response.sqid; +} + +function expectOrderedHistory(values: readonly string[], expected: readonly string[]): void { + let position = 0; + for (const value of values) { + if (value.includes(expected[position] ?? "\u0000")) { + position += 1; + } + } + expect(position).toBe(expected.length); +} + +test("challenges anonymous requests", async () => { + const anonymous = await playwrightRequest.newContext({ baseURL: webURL }); + try { + const response = await anonymous.get("/requests"); + expect(response.status()).toBe(401); + expect(response.headers()["www-authenticate"]).toContain("Basic"); + } finally { + await anonymous.dispose(); + } +}); + +test("shows newest requests and an accessible encoded detail history", async ({ page }) => { + const firstSqid = await submitRequest( + "git://git.example.com/demo/refs%2Fheads%2Fweb-first/1111111111111111111111111111111111111111?sq-files=web/first.txt", + ); + const secondSqid = await submitRequest( + "git://git.example.com/demo/refs%2Fheads%2Fweb-second/2222222222222222222222222222222222222222?sq-files=web/second.txt", + ); + + await page.goto("/requests"); + const requestLinks = page.locator(".sq-request-list__item a"); + await expect(requestLinks).toHaveCount(2, { timeout: 0 }); + await expect(requestLinks.nth(0)).toHaveText(secondSqid); + await expect(requestLinks.nth(1)).toHaveText(firstSqid); + + const listAccessibility = await new AxeBuilder({ page }).analyze(); + expect(listAccessibility.violations).toEqual([]); + + await requestLinks.nth(0).click(); + await expect(page.getByRole("heading", { name: "Request status" })).toBeVisible(); + await expect(page.getByText(secondSqid, { exact: true })).toBeVisible(); + await expect(page.locator(".sq-request-detail .sq-status").first()).toHaveText("Landed"); + + const encodedSegment = new URL(page.url()).pathname.split("/").at(-1); + expect(encodedSegment).toMatch(/^[A-Za-z0-9_-]+$/u); + expect(encodedSegment).not.toContain("/"); + + const history = await page.locator(".sq-history__item").allTextContents(); + expectOrderedHistory(history, [ + "Accepted", + "Started", + "Validating", + "Batched", + "Speculating", + "Building", + "Built", + "Speculated", + "Landing", + "Landed", + ]); + + const detailAccessibility = await new AxeBuilder({ page }).analyze(); + expect(detailAccessibility.violations).toEqual([]); +}); diff --git a/web/go.mod b/web/go.mod new file mode 100644 index 000000000..47cc235d9 --- /dev/null +++ b/web/go.mod @@ -0,0 +1,3 @@ +module github.com/uber/submitqueue/web + +go 1.25.0 diff --git a/web/package.json b/web/package.json new file mode 100644 index 000000000..350c327cc --- /dev/null +++ b/web/package.json @@ -0,0 +1,33 @@ +{ + "name": "submitqueue-web-workspace", + "private": true, + "version": "0.0.0", + "packageManager": "pnpm@10.17.1", + "engines": { + "node": ">=22.20.0 <23", + "pnpm": ">=10.17.1 <11" + }, + "scripts": { + "api:generate": "node ./tool/generate-api.mjs", + "api:check": "node ./tool/generate-api.mjs --check", + "build": "pnpm --recursive --if-present run build", + "check": "pnpm api:check && pnpm build && pnpm package:check && pnpm lint && pnpm typecheck && pnpm test", + "e2e": "playwright test", + "lint": "pnpm --recursive --if-present run lint", + "package:check": "node ./tool/check-packages.mjs", + "test": "pnpm --recursive --if-present run test", + "typecheck": "tsc --project tsconfig.e2e.json && pnpm --recursive --if-present run typecheck" + }, + "devDependencies": { + "@axe-core/playwright": "4.10.2", + "@bufbuild/buf": "1.73.0", + "@bufbuild/protoc-gen-es": "2.15.0", + "@connectrpc/connect": "2.1.1", + "@connectrpc/connect-node": "2.1.1", + "@playwright/test": "1.55.1", + "@submitqueue/api": "workspace:*", + "@types/node": "22.18.6", + "typescript": "5.9.3", + "vitest": "4.0.7" + } +} diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 000000000..67c105ce6 --- /dev/null +++ b/web/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "@playwright/test"; + +const baseURL = process.env.SUBMITQUEUE_E2E_WEB_URL; +const gatewayURL = process.env.SUBMITQUEUE_E2E_GATEWAY_URL; +const token = process.env.SUBMITQUEUE_WEB_TOKEN; + +if (!baseURL || !gatewayURL || !token) { + throw new Error( + "SUBMITQUEUE_E2E_WEB_URL, SUBMITQUEUE_E2E_GATEWAY_URL, and SUBMITQUEUE_WEB_TOKEN are required", + ); +} + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + workers: 1, + reporter: "line", + use: { + baseURL, + httpCredentials: { + username: "submitqueue", + password: token, + }, + trace: "retain-on-failure", + }, +}); diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 000000000..b124d96a3 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,5968 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@axe-core/playwright': + specifier: 4.10.2 + version: 4.10.2(playwright-core@1.55.1) + '@bufbuild/buf': + specifier: 1.73.0 + version: 1.73.0 + '@bufbuild/protoc-gen-es': + specifier: 2.15.0 + version: 2.15.0(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect': + specifier: 2.1.1 + version: 2.1.1(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect-node': + specifier: 2.1.1 + version: 2.1.1(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.15.0)) + '@playwright/test': + specifier: 1.55.1 + version: 1.55.1 + '@submitqueue/api': + specifier: workspace:* + version: link:api + '@types/node': + specifier: 22.18.6 + version: 22.18.6 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.0.7 + version: 4.0.7(@types/node@22.18.6)(jsdom@27.4.0) + + api: + dependencies: + '@bufbuild/protobuf': + specifier: 2.15.0 + version: 2.15.0 + + service/submitqueue: + dependencies: + '@bufbuild/protobuf': + specifier: 2.15.0 + version: 2.15.0 + '@connectrpc/connect': + specifier: ^2.1.1 + version: 2.2.0(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect-node': + specifier: ^2.1.1 + version: 2.2.0(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0)) + '@submitqueue/api': + specifier: workspace:* + version: link:../../api + '@submitqueue/web-submitqueue': + specifier: workspace:* + version: link:../../submitqueue + next: + specifier: '>=16.3.0 <17' + version: 16.3.6(@babel/core@7.29.7)(@playwright/test@1.55.1)(@types/node@22.18.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react: + specifier: ^19.2.0 + version: 19.3.0 + react-dom: + specifier: ^19.2.0 + version: 19.3.0(react@19.3.0) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + devDependencies: + '@types/node': + specifier: 22.18.6 + version: 22.18.6 + '@types/react': + specifier: ^19.1.16 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.1.9 + version: 19.3.0(@types/react@19.3.0) + eslint: + specifier: ^9.36.0 + version: 9.39.5 + eslint-config-next: + specifier: '>=16.3.0 <17' + version: 16.3.6(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.0.7 + version: 4.0.7(@types/node@22.18.6)(jsdom@27.4.0) + + submitqueue: + dependencies: + '@bufbuild/protobuf': + specifier: '>=2 <3' + version: 2.15.0 + '@submitqueue/api': + specifier: workspace:* + version: link:../api + devDependencies: + '@connectrpc/connect': + specifier: ^2.1.1 + version: 2.2.0(@bufbuild/protobuf@2.15.0) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.10.0(@testing-library/dom@10.4.2) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/react': + specifier: ^19.1.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.3.0(@types/react@19.3.0) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + next: + specifier: ^16.3.0 + version: 16.3.6(@babel/core@7.29.7)(@playwright/test@1.55.1)(@types/node@22.18.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react: + specifier: ^19.1.0 + version: 19.3.0 + react-dom: + specifier: ^19.1.0 + version: 19.3.0(react@19.3.0) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.1.2 + version: 3.2.7(@types/node@22.18.6)(jsdom@26.1.0) + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@axe-core/playwright@4.10.2': + resolution: {integrity: sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==} + peerDependencies: + playwright-core: '>= 1.0.0' + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bufbuild/buf-darwin-arm64@1.73.0': + resolution: {integrity: sha512-cDd3y376ecClhyytNLlY3AtsZqsqXvEkQjMvrahrv/xUWWhEIX3efD8y0uufXDe8Yuk3sEmj7J1Xlx73Y46SkA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@bufbuild/buf-darwin-x64@1.73.0': + resolution: {integrity: sha512-VTBTCwaqT7otsMGXlk3MRu6x5hp+YH9PaLKcyWAa8wEt0NnMzbXdkE8GiLgI8Qkl7S3+pC9YSTCE/I+O87Fvrw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@bufbuild/buf-linux-aarch64@1.73.0': + resolution: {integrity: sha512-HjoQ3wbLiUv5N5XLM3EAR2FY51Yn6t5MEXLPnS/Hx0KDkK/5eyIBZZ6toHBLjczAr1RTA1LagF38+lglZI+kvA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@bufbuild/buf-linux-armv7@1.73.0': + resolution: {integrity: sha512-NsZuVg8hXfGcR96Is14ArKxZg6pdJb6kb63bx2g7q8Cjo+gRqZMBFKGvF/fnQMxTrRQAm6hzeF4a2vV4W3lszg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@bufbuild/buf-linux-x64@1.73.0': + resolution: {integrity: sha512-dMe6lBQrlHTU7DzqzaR7Udky7R78EDzzxtYZQdjkZun9jKdjGR4fLbPWEPyY4rLGVyk6UUnrtkf5qoIK2SC6qw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@bufbuild/buf-win32-arm64@1.73.0': + resolution: {integrity: sha512-UmubCflc8zJ48bVW0WeGeVTkNuumJP//wCWKUmeTXT1iNlEqkpaXy6y7o8YJKHGWLfX0/1UV3oay9ti0EyojMw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@bufbuild/buf-win32-x64@1.73.0': + resolution: {integrity: sha512-/Buo10rXzqNLnFDh3BpzGdcrx6OalcU8o8gUyObCFZA5wKcUvBVNDhhpMmoB6YeiOPpPn47jHgW4bSSlypaaOQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@bufbuild/buf@1.73.0': + resolution: {integrity: sha512-W3whb4P1y3Hs9bqwCu9++W1v1ofHZ3dBUCe6JSZq0Ff5OK5UKKZuMN4iunPbgdyXY4+4ujEYf2ESrN/6MbcFSQ==} + engines: {node: '>=12'} + hasBin: true + + '@bufbuild/protobuf@2.15.0': + resolution: {integrity: sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==} + + '@bufbuild/protoc-gen-es@2.15.0': + resolution: {integrity: sha512-FRhNTAZEXW6arWx2JxgtpuazhmewYctXugu5SfXGpXpLLCInDT1005fSKDzEM/70Es2oIn07PM4RvpwvDvR/fg==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + '@bufbuild/protobuf': 2.15.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + + '@bufbuild/protoplugin@2.15.0': + resolution: {integrity: sha512-vZhRoqB/jTHATqItRvJQf1zbxdrGRRhiusGty4+lKcfEFODG0PMXky2Ezv4WHHS6oSxq+5qShITuE7K1/82SuQ==} + + '@connectrpc/connect-node@2.1.1': + resolution: {integrity: sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==} + engines: {node: '>=20'} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + '@connectrpc/connect': 2.1.1 + + '@connectrpc/connect-node@2.2.0': + resolution: {integrity: sha512-E6uUO6e11WdynAuBjuN+0WpHjqxEM9BNDdfDIJHcn49Esg/WxSIbn2y1KFt9XL96IJi01qdBbvubcdpBz1X3Zw==} + engines: {node: '>=22'} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + '@connectrpc/connect': 2.2.0 + + '@connectrpc/connect@2.1.1': + resolution: {integrity: sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + + '@connectrpc/connect@2.2.0': + resolution: {integrity: sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-calc@3.4.1': + resolution: {integrity: sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.1 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@4.2.4': + resolution: {integrity: sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.1 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@4.0.1': + resolution: {integrity: sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.14': + resolution: {integrity: sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@next/env@16.3.6': + resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} + + '@next/eslint-plugin-next@16.3.6': + resolution: {integrity: sha512-jowwDX+7DOlDIjJLgTMxudw+k37QnWu1JkZLkSi9MaJBfDYcfhAPMKBhXL0idYzFN/AGg//axnOR4cLkHX/Rng==} + + '@next/swc-darwin-arm64@16.3.6': + resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.6': + resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.6': + resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.3.6': + resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.3.6': + resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.3.6': + resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.3.6': + resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.6': + resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@playwright/test@1.55.1': + resolution: {integrity: sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==} + engines: {node: '>=18'} + hasBin: true + + '@rollup/rollup-android-arm-eabi@4.63.5': + resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.5': + resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.5': + resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.5': + resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.5': + resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.5': + resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.5': + resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.63.5': + resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.63.5': + resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.63.5': + resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.63.5': + resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.63.5': + resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.63.5': + resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.63.5': + resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.63.5': + resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.63.5': + resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.63.5': + resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.63.5': + resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.63.5': + resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.63.5': + resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.5': + resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.5': + resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.5': + resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.5': + resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.5': + resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + 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 + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.18.6': + resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript-eslint/eslint-plugin@8.71.0': + resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.71.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.71.0': + resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.71.0': + resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.71.0': + resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.71.0': + resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.71.0': + resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.71.0': + resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.71.0': + resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.71.0': + resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.71.0': + resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/vfs@1.6.5': + resolution: {integrity: sha512-gobLHQLmvQGJInFk31NvgMd1uYzRXsd8qgnOssyLD055R2QPnw1ubRbBQ4+T+5qC7IQVuYXdvNDE2G7tMDVHgw==} + peerDependencies: + typescript: '*' + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/expect@4.0.7': + resolution: {integrity: sha512-jGRG6HghnJDjljdjYIoVzX17S6uCVCBRFnsgdLGJ6CaxfPh8kzUKe/2n533y4O/aeZ/sIr7q7GbuEbeGDsWv4Q==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/mocker@4.0.7': + resolution: {integrity: sha512-OsDwLS7WnpuNslOV6bJkXVYVV/6RSc4eeVxV7h9wxQPNxnjRvTTrIikfwCbMyl8XJmW6oOccBj2Q07YwZtQcCw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/pretty-format@4.0.7': + resolution: {integrity: sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/runner@4.0.7': + resolution: {integrity: sha512-orU1lsu4PxLEcDWfjVCNGIedOSF/YtZ+XMrd1PZb90E68khWCNzD8y1dtxtgd0hyBIQk8XggteKN/38VQLvzuw==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/snapshot@4.0.7': + resolution: {integrity: sha512-xJL+Nkw0OjaUXXQf13B8iKK5pI9QVtN9uOtzNHYuG/o/B7fIEg0DQ+xOe0/RcqwDEI15rud1k7y5xznBKGUXAA==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/spy@4.0.7': + resolution: {integrity: sha512-FW4X8hzIEn4z+HublB4hBF/FhCVaXfIHm8sUfvlznrcy1MQG7VooBgZPMtVCGZtHi0yl3KESaXTqsKh16d8cFg==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + '@vitest/utils@4.0.7': + resolution: {integrity: sha512-HNrg9CM/Z4ZWB6RuExhuC6FPmLipiShKVMnT9JlQvfhwR47JatWLChA6mtZqVHqypE6p/z6ofcjbyWpM7YLxPQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.26: + resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.29.2: + resolution: {integrity: sha512-/u9r8k8ue4ZhHCw9ovDtltpWdXeXzjhdxCGj6vm1RGLPXb8lu33EMdoCHF5Sx8oYBS+Q/FYYNmAqlHncxR3RmA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001813: + resolution: {integrity: sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.439: + resolution: {integrity: sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@16.3.6: + resolution: {integrity: sha512-1Upt3U7BDwU+ilpe2byZjAfts9oNq4d4fv/zXEvs8/4yS+cwOQW/WCxUNy8gCDquX67SzeehDvKblVC6ZBMocQ==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.10: + resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@16.3.6: + resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} + engines: {node: '>=18'} + + nwsapi@2.2.28: + resolution: {integrity: sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.55.1: + resolution: {integrity: sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.55.1: + resolution: {integrity: sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==} + engines: {node: '>=18'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.63.5: + resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.4.16: + resolution: {integrity: sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.4.16: + resolution: {integrity: sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.71.0: + resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + 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 + + vitest@4.0.7: + resolution: {integrity: sha512-xQroKAadK503CrmbzCISvQUjeuvEZzv6U0wlnlVFOi5i3gnzfH4onyQ29f3lzpe0FresAiTAd3aqK0Bi/jLI8w==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.7 + '@vitest/browser-preview': 4.0.7 + '@vitest/browser-webdriverio': 4.0.7 + '@vitest/ui': 4.0.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + 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 + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + +snapshots: + + '@acemir/cssom@0.9.31': + optional: true + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 + optional: true + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + optional: true + + '@asamuzakjp/nwsapi@2.3.9': + optional: true + + '@axe-core/playwright@4.10.2(playwright-core@1.55.1)': + dependencies: + axe-core: 4.10.3 + playwright-core: 1.55.1 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bufbuild/buf-darwin-arm64@1.73.0': + optional: true + + '@bufbuild/buf-darwin-x64@1.73.0': + optional: true + + '@bufbuild/buf-linux-aarch64@1.73.0': + optional: true + + '@bufbuild/buf-linux-armv7@1.73.0': + optional: true + + '@bufbuild/buf-linux-x64@1.73.0': + optional: true + + '@bufbuild/buf-win32-arm64@1.73.0': + optional: true + + '@bufbuild/buf-win32-x64@1.73.0': + optional: true + + '@bufbuild/buf@1.73.0': + optionalDependencies: + '@bufbuild/buf-darwin-arm64': 1.73.0 + '@bufbuild/buf-darwin-x64': 1.73.0 + '@bufbuild/buf-linux-aarch64': 1.73.0 + '@bufbuild/buf-linux-armv7': 1.73.0 + '@bufbuild/buf-linux-x64': 1.73.0 + '@bufbuild/buf-win32-arm64': 1.73.0 + '@bufbuild/buf-win32-x64': 1.73.0 + + '@bufbuild/protobuf@2.15.0': {} + + '@bufbuild/protoc-gen-es@2.15.0(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protoplugin': 2.15.0 + optionalDependencies: + '@bufbuild/protobuf': 2.15.0 + transitivePeerDependencies: + - supports-color + + '@bufbuild/protoplugin@2.15.0': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@typescript/vfs': 1.6.5(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@connectrpc/connect-node@2.1.1(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.15.0))': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@connectrpc/connect': 2.1.1(@bufbuild/protobuf@2.15.0) + + '@connectrpc/connect-node@2.2.0(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0))': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@connectrpc/connect': 2.2.0(@bufbuild/protobuf@2.15.0) + + '@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protobuf': 2.15.0 + + '@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protobuf': 2.15.0 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/color-helpers@6.1.2': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-calc@3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + optional: true + + '@csstools/css-syntax-patches-for-csstree@1.1.14(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + optional: true + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/css-tokenizer@4.0.2': + optional: true + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)': + dependencies: + eslint: 9.39.5 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5)': + dependencies: + eslint: 9.39.5 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.7': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@exodus/bytes@1.16.0': + optional: true + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@next/env@16.3.6': {} + + '@next/eslint-plugin-next@16.3.6(eslint@9.39.5)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5) + fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint + + '@next/swc-darwin-arm64@16.3.6': + optional: true + + '@next/swc-darwin-x64@16.3.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.6': + optional: true + + '@next/swc-linux-arm64-musl@16.3.6': + optional: true + + '@next/swc-linux-x64-gnu@16.3.6': + optional: true + + '@next/swc-linux-x64-musl@16.3.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.6': + optional: true + + '@next/swc-win32-x64-msvc@16.3.6': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@nolyfill/is-core-module@1.0.39': {} + + '@playwright/test@1.55.1': + dependencies: + playwright: 1.55.1 + + '@rollup/rollup-android-arm-eabi@4.63.5': + optional: true + + '@rollup/rollup-android-arm64@4.63.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.5': + optional: true + + '@rollup/rollup-darwin-x64@4.63.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.5': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.5': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.2)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.18.6': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/type-utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 + eslint: 9.39.5 + ignore: 7.0.10 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3 + eslint: 9.39.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.71.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + + '@typescript-eslint/tsconfig-utils@8.71.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.71.0(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.5 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.71.0': {} + + '@typescript-eslint/typescript-estree@8.71.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.71.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.71.0(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + eslint: 9.39.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + eslint-visitor-keys: 5.0.1 + + '@typescript/vfs@1.6.5(typescript@5.4.5)': + dependencies: + debug: 4.4.3 + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/expect@4.0.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.7 + '@vitest/utils': 4.0.7 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.18.6))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.18.6) + + '@vitest/mocker@4.0.7(vite@7.3.6(@types/node@22.18.6))': + dependencies: + '@vitest/spy': 4.0.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.18.6) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@4.0.7': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/runner@4.0.7': + dependencies: + '@vitest/utils': 4.0.7 + pathe: 2.0.3 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.7': + dependencies: + '@vitest/pretty-format': 4.0.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/spy@4.0.7': {} + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@vitest/utils@4.0.7': + dependencies: + '@vitest/pretty-format': 4.0.7 + tinyrainbow: 3.1.1 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axe-core@4.13.0: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.26: {} + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + optional: true + + brace-expansion@1.1.21: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.29.2: + dependencies: + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001813 + electron-to-chromium: 1.5.439 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.2) + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001813: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + optional: true + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.3 + optional: true + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + optional: true + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dequal@2.0.3: {} + + detect-libc@2.1.2: + optional: true + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.439: {} + + emoji-regex@9.2.2: {} + + entities@6.0.1: {} + + entities@8.1.0: + optional: true + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.5 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.24 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.28.2: + 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 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.3.6(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.3.6(eslint@9.39.5) + eslint: 9.39.5 + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5) + eslint-plugin-react: 7.37.5(eslint@9.39.5) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5) + globals: 16.4.0 + typescript-eslint: 8.71.0(eslint@9.39.5)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.17.0 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.5 + get-tsconfig: 4.14.3 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + eslint: 9.39.5 + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.2.0 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.5 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + hasown: 2.0.4 + is-core-module: 2.17.0 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5): + dependencies: + aria-query: 5.3.2 + array-includes: 3.2.0 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.13.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.5 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.9 + eslint: 9.39.5 + hermes-parser: 0.25.1 + zod: 4.6.5 + zod-validation-error: 4.0.2(zod@4.6.5) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.5): + dependencies: + array-includes: 3.2.0 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.4.0 + eslint: 9.39.5 + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.1.0 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.7 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.16.0 + transitivePeerDependencies: + - '@noble/hashes' + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.10: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + + is-callable@1.2.7: {} + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.24 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.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.28 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.22.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.16.0 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.22.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.2.0 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.5.3: + optional: true + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: + optional: true + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + min-indent@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.21 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@16.3.6(@babel/core@7.29.7)(@playwright/test@1.55.1)(@types/node@22.18.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@next/env': 16.3.6 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001813 + postcss: 8.5.23 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.3.0) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.6 + '@next/swc-darwin-x64': 16.3.6 + '@next/swc-linux-arm64-gnu': 16.3.6 + '@next/swc-linux-arm64-musl': 16.3.6 + '@next/swc-linux-x64-gnu': 16.3.6 + '@next/swc-linux-x64-musl': 16.3.6 + '@next/swc-win32-arm64-msvc': 16.3.6 + '@next/swc-win32-x64-msvc': 16.3.6 + '@playwright/test': 1.55.1 + sharp: 0.35.5(@types/node@22.18.6) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.57: {} + + nwsapi@2.2.28: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.1: + dependencies: + entities: 8.1.0 + optional: true + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + playwright-core@1.55.1: {} + + playwright@1.55.1: + dependencies: + playwright-core: 1.55.1 + optionalDependencies: + fsevents: 2.3.2 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react@19.3.0: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: + optional: true + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.63.5: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.5 + '@rollup/rollup-android-arm64': 4.63.5 + '@rollup/rollup-darwin-arm64': 4.63.5 + '@rollup/rollup-darwin-x64': 4.63.5 + '@rollup/rollup-freebsd-arm64': 4.63.5 + '@rollup/rollup-freebsd-x64': 4.63.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.5 + '@rollup/rollup-linux-arm-musleabihf': 4.63.5 + '@rollup/rollup-linux-arm64-gnu': 4.63.5 + '@rollup/rollup-linux-arm64-musl': 4.63.5 + '@rollup/rollup-linux-loong64-gnu': 4.63.5 + '@rollup/rollup-linux-loong64-musl': 4.63.5 + '@rollup/rollup-linux-ppc64-gnu': 4.63.5 + '@rollup/rollup-linux-ppc64-musl': 4.63.5 + '@rollup/rollup-linux-riscv64-gnu': 4.63.5 + '@rollup/rollup-linux-riscv64-musl': 4.63.5 + '@rollup/rollup-linux-s390x-gnu': 4.63.5 + '@rollup/rollup-linux-x64-gnu': 4.63.5 + '@rollup/rollup-linux-x64-musl': 4.63.5 + '@rollup/rollup-openbsd-x64': 4.63.5 + '@rollup/rollup-openharmony-arm64': 4.63.5 + '@rollup/rollup-win32-arm64-msvc': 4.63.5 + '@rollup/rollup-win32-ia32-msvc': 4.63.5 + '@rollup/rollup-win32-x64-gnu': 4.63.5 + '@rollup/rollup-win32-x64-msvc': 4.63.5 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + sharp@0.35.5(@types/node@22.18.6): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 22.18.6 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.1.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.3.0): + dependencies: + client-only: 0.0.1 + react: 19.3.0 + optionalDependencies: + '@babel/core': 7.29.7 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyrainbow@3.1.1: {} + + tinyspy@4.0.6: {} + + tldts-core@6.1.86: {} + + tldts-core@7.4.16: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.4.16: + dependencies: + tldts-core: 7.4.16 + optional: true + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.16 + optional: true + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + optional: true + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.5: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.71.0(eslint@9.39.5)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + eslint: 9.39.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.4.5: {} + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.3.3(browserslist@4.29.2): + dependencies: + browserslist: 4.29.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(@types/node@22.18.6): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.18.6) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.18.6): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.18.6 + fsevents: 2.3.3 + + vitest@3.2.7(@types/node@22.18.6)(jsdom@26.1.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.18.6)) + '@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.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.18.6) + vite-node: 3.2.4(@types/node@22.18.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.18.6 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@4.0.7(@types/node@22.18.6)(jsdom@27.4.0): + dependencies: + '@vitest/expect': 4.0.7 + '@vitest/mocker': 4.0.7(vite@7.3.6(@types/node@22.18.6)) + '@vitest/pretty-format': 4.0.7 + '@vitest/runner': 4.0.7 + '@vitest/snapshot': 4.0.7 + '@vitest/spy': 4.0.7 + '@vitest/utils': 4.0.7 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@22.18.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.18.6 + jsdom: 27.4.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + webidl-conversions@8.0.1: + optional: true + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: + optional: true + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + optional: true + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.24 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.24: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + ws@8.22.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.6.5): + dependencies: + zod: 4.6.5 + + zod@4.6.5: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 000000000..3d8f3cbcc --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - api + - submitqueue + - service/* + +onlyBuiltDependencies: + - '@bufbuild/buf' + - esbuild diff --git a/web/service/submitqueue/.gitignore b/web/service/submitqueue/.gitignore new file mode 100644 index 000000000..5c560c0c2 --- /dev/null +++ b/web/service/submitqueue/.gitignore @@ -0,0 +1,3 @@ +.next/ +node_modules/ +tsconfig.tsbuildinfo diff --git a/web/service/submitqueue/Dockerfile b/web/service/submitqueue/Dockerfile new file mode 100644 index 000000000..dc0fc68cb --- /dev/null +++ b/web/service/submitqueue/Dockerfile @@ -0,0 +1,38 @@ +FROM node:22.20.0-bookworm-slim AS dependencies + +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable && corepack prepare pnpm@10.17.1 --activate + +WORKDIR /workspace + +COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml web/tsconfig.base.json ./ +COPY web/api/package.json ./api/package.json +COPY web/submitqueue/package.json ./submitqueue/package.json +COPY web/service/submitqueue/package.json ./service/submitqueue/package.json +RUN pnpm install --frozen-lockfile + +FROM dependencies AS build + +COPY web/api ./api +COPY web/submitqueue ./submitqueue +COPY web/service/submitqueue ./service/submitqueue +RUN pnpm --filter @submitqueue/api build \ + && pnpm --filter @submitqueue/web-submitqueue build \ + && pnpm --filter @submitqueue/service-submitqueue-web build + +FROM node:22.20.0-bookworm-slim AS runtime + +ENV HOSTNAME=0.0.0.0 +ENV NODE_ENV=production +ENV PORT=3000 + +WORKDIR /app + +COPY --chown=node:node --from=build /workspace/service/submitqueue/.next/standalone ./ +COPY --chown=node:node --from=build /workspace/service/submitqueue/.next/static ./service/submitqueue/.next/static + +EXPOSE 3000 + +USER node +CMD ["node", "service/submitqueue/server.js"] diff --git a/web/service/submitqueue/eslint.config.mjs b/web/service/submitqueue/eslint.config.mjs new file mode 100644 index 000000000..db21edbe5 --- /dev/null +++ b/web/service/submitqueue/eslint.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; + +export default defineConfig([ + ...nextVitals, + globalIgnores([".next/**", "node_modules/**"]), +]); diff --git a/web/service/submitqueue/next-env.d.ts b/web/service/submitqueue/next-env.d.ts new file mode 100644 index 000000000..ce4e94a6b --- /dev/null +++ b/web/service/submitqueue/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/web/service/submitqueue/next.config.ts b/web/service/submitqueue/next.config.ts new file mode 100644 index 000000000..44eba9eb6 --- /dev/null +++ b/web/service/submitqueue/next.config.ts @@ -0,0 +1,13 @@ +import type { NextConfig } from "next"; +import path from "node:path"; + +const nextConfig: NextConfig = { + experimental: { + authInterrupts: true, + }, + output: "standalone", + outputFileTracingRoot: path.join(import.meta.dirname, "../.."), + transpilePackages: ["@submitqueue/api", "@submitqueue/web-submitqueue"], +}; + +export default nextConfig; diff --git a/web/service/submitqueue/package.json b/web/service/submitqueue/package.json new file mode 100644 index 000000000..545fbb8d7 --- /dev/null +++ b/web/service/submitqueue/package.json @@ -0,0 +1,34 @@ +{ + "name": "@submitqueue/service-submitqueue-web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "next build", + "dev": "next dev --hostname 0.0.0.0 --port 3000", + "lint": "eslint .", + "start": "next start --hostname 0.0.0.0 --port 3000", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@bufbuild/protobuf": "2.15.0", + "@connectrpc/connect": "^2.1.1", + "@connectrpc/connect-node": "^2.1.1", + "@submitqueue/api": "workspace:*", + "@submitqueue/web-submitqueue": "workspace:*", + "next": ">=16.3.0 <17", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@types/node": "22.18.6", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "eslint": "^9.36.0", + "eslint-config-next": ">=16.3.0 <17", + "typescript": "5.9.3", + "vitest": "4.0.7" + } +} diff --git a/web/service/submitqueue/src/app/(protected)/layout.tsx b/web/service/submitqueue/src/app/(protected)/layout.tsx new file mode 100644 index 000000000..5ed016675 --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/layout.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; + +import { requireAuthorization } from "../../server/request-auth"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export default async function ProtectedLayout({ + children, +}: Readonly<{ children: ReactNode }>) { + await requireAuthorization(); + + return ( +
+
+ + + SubmitQueue + + demo-queue +
+ {children} +
+ Read-only demo Β· updates automatically +
+
+ ); +} diff --git a/web/service/submitqueue/src/app/(protected)/page.tsx b/web/service/submitqueue/src/app/(protected)/page.tsx new file mode 100644 index 000000000..4be8ef69f --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/page.tsx @@ -0,0 +1,18 @@ +import { connection } from "next/server"; +import { redirect } from "next/navigation"; + +import { requireAuthorization } from "../../server/request-auth"; +import { + defaultRequestWindow, + requestWindowSearch, +} from "../../server/window"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export default async function HomePage() { + await connection(); + await requireAuthorization(); + const search = requestWindowSearch(defaultRequestWindow()); + redirect(`/requests?${search.toString()}`); +} diff --git a/web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx b/web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx new file mode 100644 index 000000000..12c1e29f0 --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx @@ -0,0 +1,87 @@ +import { + AutoRefresh, + ErrorState, + RequestDetail, +} from "@submitqueue/web-submitqueue"; +import { + isTerminalStatus, + loadRequestDetail, + WebPaths, +} from "@submitqueue/web-submitqueue/server"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { connection } from "next/server"; + +import { DEMO_QUEUE } from "../../../../server/config"; +import { resolveDemoGateway } from "../../../../server/gateway"; +import { requireAuthorization } from "../../../../server/request-auth"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export const metadata: Metadata = { + title: "Request status", +}; + +const paths = new WebPaths(); + +export default async function RequestPage({ + params, +}: Readonly<{ + params: Promise<{ sqid: string }>; +}>) { + await connection(); + await requireAuthorization(); + + const sqid = paths.decodeRequest((await params).sqid); + if (!sqid) { + notFound(); + } + + const result = await loadRequestDetail(resolveDemoGateway, { + queue: DEMO_QUEUE, + sqid, + }); + + if (!result.ok) { + if (result.error.kind === "not-found") { + notFound(); + } + return ( +
+
+
+

Request status

+

Unable to load request

+
+
+
+ + +
+
+ ); + } + + return ( +
+
+
+

+ Requests / Detail +

+

Request status

+

{result.data.request.sqid}

+
+ +
+ +
+ ); +} diff --git a/web/service/submitqueue/src/app/(protected)/requests/page.tsx b/web/service/submitqueue/src/app/(protected)/requests/page.tsx new file mode 100644 index 000000000..3f6896436 --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/requests/page.tsx @@ -0,0 +1,79 @@ +import { + AutoRefresh, + ErrorState, + RequestList, +} from "@submitqueue/web-submitqueue"; +import { loadRequestList } from "@submitqueue/web-submitqueue/server"; +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { connection } from "next/server"; + +import { DEMO_QUEUE } from "../../../server/config"; +import { resolveDemoGateway } from "../../../server/gateway"; +import { requireAuthorization } from "../../../server/request-auth"; +import { + defaultRequestWindow, + parseRequestWindow, + REQUEST_PAGE_SIZE, + requestWindowSearch, + type RequestSearchParams, +} from "../../../server/window"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export const metadata: Metadata = { + title: "Requests", +}; + +export default async function RequestsPage({ + searchParams, +}: Readonly<{ + searchParams: Promise; +}>) { + await connection(); + await requireAuthorization(); + + const requestWindow = parseRequestWindow(await searchParams); + if (!requestWindow) { + const canonical = requestWindowSearch(defaultRequestWindow()); + redirect(`/requests?${canonical.toString()}`); + } + + const result = await loadRequestList(resolveDemoGateway, { + queue: DEMO_QUEUE, + receivedAtOrAfterMs: requestWindow.fromMs, + receivedBeforeMs: requestWindow.toMs, + pageSize: REQUEST_PAGE_SIZE, + pageToken: requestWindow.pageToken, + }); + + return ( +
+
+
+

Queue activity

+

Requests

+

+ Requests received in the fixed 24-hour demo window, newest first. +

+
+ +
+ + {result.ok ? ( +
+ +
+ ) : ( +
+ + +
+ )} +
+ ); +} diff --git a/web/service/submitqueue/src/app/globals.css b/web/service/submitqueue/src/app/globals.css new file mode 100644 index 000000000..0b31ece92 --- /dev/null +++ b/web/service/submitqueue/src/app/globals.css @@ -0,0 +1,505 @@ +:root { + color-scheme: light; + --bg: #f5f7fb; + --surface: #ffffff; + --surface-muted: #eef2f7; + --text: #172033; + --muted: #667085; + --border: #d8dee9; + --accent: #2457d6; + --accent-dark: #183f9f; + --success: #087a4b; + --warning: #a65f00; + --danger: #c9362b; + --shadow: 0 16px 48px rgb(21 35 64 / 8%); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +* { + box-sizing: border-box; +} + +html { + min-width: 320px; + background: var(--bg); +} + +body { + min-height: 100vh; + margin: 0; + color: var(--text); + background: + radial-gradient(circle at 10% 0%, rgb(36 87 214 / 9%), transparent 28rem), + var(--bg); +} + +a { + color: var(--accent); + text-decoration-thickness: 0.08em; + text-underline-offset: 0.16em; +} + +a:hover { + color: var(--accent-dark); +} + +a:focus-visible, +button:focus-visible { + outline: 3px solid rgb(36 87 214 / 35%); + outline-offset: 3px; +} + +.site-frame { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.site-header, +.site-footer, +.shell { + width: min(1120px, calc(100% - 40px)); + margin-inline: auto; +} + +.site-header { + min-height: 76px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + border-bottom: 1px solid rgb(216 222 233 / 80%); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 11px; + color: var(--text); + font-size: 1rem; + font-weight: 750; + letter-spacing: -0.01em; + text-decoration: none; +} + +.brand-mark { + width: 34px; + height: 34px; + display: grid; + place-items: center; + border-radius: 9px; + color: white; + background: var(--accent); + font-size: 0.75rem; + letter-spacing: 0.04em; +} + +.environment-pill, +.status-pill { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 28px; + padding: 4px 10px; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted); + background: rgb(255 255 255 / 70%); + font-size: 0.75rem; + font-weight: 650; +} + +.shell { + padding-block: 50px 72px; +} + +.centered-state { + min-height: 100vh; + display: grid; + place-items: center; +} + +.page-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 24px; + margin-bottom: 28px; +} + +.page-heading h1, +.state-card h1 { + margin: 0; + font-size: clamp(2rem, 5vw, 3.25rem); + line-height: 1.05; + letter-spacing: -0.045em; +} + +.page-heading p:not(.eyebrow), +.state-card p:not(.eyebrow) { + max-width: 64ch; + margin: 10px 0 0; + color: var(--muted); + line-height: 1.6; +} + +.eyebrow { + margin: 0 0 9px; + color: var(--accent); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.panel, +.state-card { + border: 1px solid var(--border); + border-radius: 18px; + background: rgb(255 255 255 / 92%); + box-shadow: var(--shadow); +} + +.panel { + overflow: hidden; +} + +.state-card { + max-width: 600px; + padding: clamp(28px, 6vw, 52px); +} + +.button-link { + display: inline-flex; + margin-top: 24px; + padding: 10px 15px; + border-radius: 9px; + color: white; + background: var(--accent); + font-weight: 700; + text-decoration: none; +} + +.button-link:hover { + color: white; + background: var(--accent-dark); +} + +.pagination { + display: flex; + justify-content: flex-end; + margin-top: 24px; +} + +.request-identifier { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + overflow-wrap: anywhere; +} + +.sq-refresh { + display: flex; + min-height: 38px; + align-items: center; + gap: 10px; + color: var(--muted); + font-size: 0.75rem; +} + +.sq-refresh button { + min-height: 38px; + padding: 8px 13px; + border: 1px solid var(--border); + border-radius: 9px; + color: var(--text); + background: var(--surface); + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.sq-refresh button:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.sq-refresh button:disabled { + cursor: default; + opacity: 0.6; +} + +.sq-request-list, +.sq-history, +.sq-changes { + padding: 0; + margin: 0; + list-style: none; +} + +.sq-request-list__item + .sq-request-list__item { + border-top: 1px solid var(--border); +} + +.sq-request-list__item article { + padding: 23px 26px; +} + +.sq-request-list__item header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.sq-request-list__item header a { + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.92rem; + font-weight: 750; + overflow-wrap: anywhere; +} + +.sq-request-list__item p { + margin: 12px 0 0; + color: var(--muted); + font-size: 0.82rem; +} + +.sq-changes { + display: grid; + gap: 7px; + margin-top: 15px; +} + +.sq-changes code { + display: block; + width: fit-content; + max-width: 100%; + padding: 5px 8px; + border-radius: 6px; + color: #344054; + background: var(--surface-muted); + font-size: 0.78rem; + overflow-wrap: anywhere; + white-space: normal; +} + +.sq-status, +.sq-event { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 27px; + padding: 4px 10px; + border-radius: 999px; + color: var(--accent-dark); + background: #e8efff; + font-size: 0.72rem; + font-weight: 750; +} + +.sq-status[data-tone="success"] { + color: var(--success); + background: #e5f6ee; +} + +.sq-status[data-tone="warning"] { + color: var(--warning); + background: #fff2da; +} + +.sq-status[data-tone="danger"] { + color: var(--danger); + background: #ffebe9; +} + +.sq-empty { + padding: 52px 26px; + color: var(--muted); + text-align: center; +} + +.sq-error h2 { + margin: 0; + font-size: 1.25rem; +} + +.sq-error p { + margin: 8px 0 0; + color: var(--muted); + line-height: 1.55; +} + +.sq-request-detail { + display: grid; + gap: 18px; +} + +.sq-request-detail > header, +.sq-request-detail > section, +.sq-request-detail > .sq-metadata { + padding: 26px; + border: 1px solid var(--border); + border-radius: 16px; + background: rgb(255 255 255 / 92%); + box-shadow: var(--shadow); +} + +.sq-request-detail > header > p:first-child { + margin-top: 0; + color: var(--muted); + font-size: 0.78rem; + font-weight: 750; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.sq-request-detail > header h1 { + margin: 9px 0 18px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: clamp(1.15rem, 4vw, 1.75rem); + overflow-wrap: anywhere; +} + +.sq-request-detail > header > p:last-child { + margin: 16px 0 0; + color: var(--muted); + font-size: 0.82rem; +} + +.sq-request-detail section h2 { + margin: 0 0 18px; + font-size: 1rem; +} + +.sq-metadata { + display: grid; + gap: 1px; + margin: 18px 0 0; +} + +.sq-metadata__entry { + display: grid; + grid-template-columns: minmax(120px, 0.35fr) 1fr; + gap: 16px; + padding: 9px 0; + border-bottom: 1px solid var(--border); + font-size: 0.8rem; +} + +.sq-metadata__entry:last-child { + border-bottom: 0; +} + +.sq-metadata dt { + color: var(--muted); + font-weight: 700; +} + +.sq-metadata dd { + margin: 0; + overflow-wrap: anywhere; +} + +.sq-history { + display: grid; + gap: 0; +} + +.sq-history__item { + position: relative; + display: grid; + grid-template-columns: minmax(170px, 0.35fr) 1fr; + gap: 12px 22px; + padding: 17px 0 17px 21px; + border-left: 2px solid var(--border); +} + +.sq-history__item::before { + position: absolute; + top: 22px; + left: -6px; + width: 10px; + height: 10px; + border: 2px solid var(--surface); + border-radius: 50%; + background: var(--accent); + content: ""; +} + +.sq-history__item time { + color: var(--muted); + font-size: 0.78rem; +} + +.sq-history__item p, +.sq-history__item .sq-metadata { + grid-column: 2; +} + +.site-footer { + padding-block: 22px 30px; + border-top: 1px solid rgb(216 222 233 / 80%); + color: var(--muted); + font-size: 0.78rem; +} + +@media (max-width: 680px) { + .site-header, + .site-footer, + .shell { + width: min(100% - 24px, 1120px); + } + + .site-header { + min-height: 64px; + } + + .shell { + padding-block: 34px 52px; + } + + .page-heading { + align-items: start; + flex-direction: column; + } + + .environment-pill { + max-width: 132px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .sq-request-list__item article, + .sq-request-detail > header, + .sq-request-detail > section, + .sq-request-detail > .sq-metadata { + padding: 19px; + } + + .sq-request-list__item header { + align-items: flex-start; + flex-direction: column; + } + + .sq-metadata__entry, + .sq-history__item { + grid-template-columns: 1fr; + } + + .sq-history__item p, + .sq-history__item .sq-metadata { + grid-column: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/web/service/submitqueue/src/app/healthz/route.test.ts b/web/service/submitqueue/src/app/healthz/route.test.ts new file mode 100644 index 000000000..fb9071cf7 --- /dev/null +++ b/web/service/submitqueue/src/app/healthz/route.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { GET } from "./route"; + +describe("GET /healthz", () => { + it("is a public no-store liveness response", async () => { + const response = GET(); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ status: "ok" }); + }); +}); diff --git a/web/service/submitqueue/src/app/healthz/route.ts b/web/service/submitqueue/src/app/healthz/route.ts new file mode 100644 index 000000000..3acb406b0 --- /dev/null +++ b/web/service/submitqueue/src/app/healthz/route.ts @@ -0,0 +1,8 @@ +export const dynamic = "force-dynamic"; + +export function GET(): Response { + return Response.json( + { status: "ok" }, + { headers: { "Cache-Control": "no-store" } }, + ); +} diff --git a/web/service/submitqueue/src/app/layout.tsx b/web/service/submitqueue/src/app/layout.tsx new file mode 100644 index 000000000..239735840 --- /dev/null +++ b/web/service/submitqueue/src/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +import "./globals.css"; + +export const metadata: Metadata = { + title: { + default: "SubmitQueue", + template: "%s Β· SubmitQueue", + }, + description: "Read-only request status for the SubmitQueue demo queue.", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/web/service/submitqueue/src/app/not-found.tsx b/web/service/submitqueue/src/app/not-found.tsx new file mode 100644 index 000000000..5d24db36d --- /dev/null +++ b/web/service/submitqueue/src/app/not-found.tsx @@ -0,0 +1,16 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+
+

Request lookup

+

Request not found

+

The request may not exist in the demo queue or may have expired.

+ + Return to requests + +
+
+ ); +} diff --git a/web/service/submitqueue/src/app/unauthorized.tsx b/web/service/submitqueue/src/app/unauthorized.tsx new file mode 100644 index 000000000..cfd1a9293 --- /dev/null +++ b/web/service/submitqueue/src/app/unauthorized.tsx @@ -0,0 +1,11 @@ +export default function Unauthorized() { + return ( +
+
+

SubmitQueue

+

Authentication required

+

Use the demo credentials configured for this host.

+
+
+ ); +} diff --git a/web/service/submitqueue/src/proxy.test.ts b/web/service/submitqueue/src/proxy.test.ts new file mode 100644 index 000000000..cd82036f7 --- /dev/null +++ b/web/service/submitqueue/src/proxy.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { BASIC_AUTH_USERNAME } from "./server/auth"; +import { proxy } from "./proxy"; + +const token = "demo-token-with-at-least-thirty-two-characters"; + +function request(authorization?: string) { + return { + headers: new Headers( + authorization ? { authorization } : undefined, + ), + } as Parameters[0]; +} + +function basic(username: string, password: string): string { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; +} + +describe("proxy", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("challenges an anonymous browser", () => { + vi.stubEnv("SUBMITQUEUE_WEB_TOKEN", token); + const response = proxy(request()); + + expect(response?.status).toBe(401); + expect(response?.headers.get("www-authenticate")).toMatch(/^Basic /u); + expect(response?.headers.get("cache-control")).toBe("no-store"); + }); + + it("allows valid demo credentials", () => { + vi.stubEnv("SUBMITQUEUE_WEB_TOKEN", token); + expect(proxy(request(basic(BASIC_AUTH_USERNAME, token)))).toBeUndefined(); + }); + + it("challenges an incorrect password", () => { + vi.stubEnv("SUBMITQUEUE_WEB_TOKEN", token); + expect( + proxy(request(basic(BASIC_AUTH_USERNAME, "incorrect-password-value-000000"))) + ?.status, + ).toBe(401); + }); +}); diff --git a/web/service/submitqueue/src/proxy.ts b/web/service/submitqueue/src/proxy.ts new file mode 100644 index 000000000..8b6dcaffe --- /dev/null +++ b/web/service/submitqueue/src/proxy.ts @@ -0,0 +1,21 @@ +import type { NextRequest } from "next/server"; + +import { + authenticationChallenge, + isAuthorized, + loadAuthConfiguration, +} from "./server/auth"; + +export function proxy(request: NextRequest): Response | undefined { + const configuration = loadAuthConfiguration(); + if (!isAuthorized(request.headers.get("authorization"), configuration)) { + return authenticationChallenge(); + } + return undefined; +} + +export const config = { + matcher: [ + "/((?!healthz$|_next/static|_next/image|favicon.ico|robots.txt).*)", + ], +}; diff --git a/web/service/submitqueue/src/server/auth.test.ts b/web/service/submitqueue/src/server/auth.test.ts new file mode 100644 index 000000000..e7b66da21 --- /dev/null +++ b/web/service/submitqueue/src/server/auth.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; + +import { + BASIC_AUTH_USERNAME, + isAuthorized, + loadAuthConfiguration, + MINIMUM_TOKEN_LENGTH, +} from "./auth"; + +const token = "a".repeat(MINIMUM_TOKEN_LENGTH); + +function basic(username: string, password: string): string { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; +} + +describe("loadAuthConfiguration", () => { + it("loads a sufficiently long token", () => { + expect(loadAuthConfiguration({ SUBMITQUEUE_WEB_TOKEN: token })).toEqual({ + token, + }); + }); + + it.each([undefined, "short"])("rejects a missing or short token", (value) => { + expect(() => + loadAuthConfiguration({ SUBMITQUEUE_WEB_TOKEN: value }), + ).toThrow(/at least 32 characters/u); + }); +}); + +describe("isAuthorized", () => { + it("accepts the configured credentials", () => { + expect( + isAuthorized(basic(BASIC_AUTH_USERNAME, token), { token }), + ).toBe(true); + }); + + it.each([ + ["missing header", null], + ["wrong scheme", `Bearer ${token}`], + ["malformed base64", "Basic !!!"], + ["missing separator", `Basic ${Buffer.from("submitqueue").toString("base64")}`], + ["wrong user", basic("operator", token)], + ["wrong password", basic(BASIC_AUTH_USERNAME, "b".repeat(32))], + ])("rejects %s", (_name, authorization) => { + expect(isAuthorized(authorization, { token })).toBe(false); + }); + + it("allows colons in the password", () => { + const password = `${token}:suffix`; + expect( + isAuthorized(basic(BASIC_AUTH_USERNAME, password), { token: password }), + ).toBe(true); + }); +}); diff --git a/web/service/submitqueue/src/server/auth.ts b/web/service/submitqueue/src/server/auth.ts new file mode 100644 index 000000000..eccf88ba6 --- /dev/null +++ b/web/service/submitqueue/src/server/auth.ts @@ -0,0 +1,86 @@ +import "server-only"; + +import { createHash, timingSafeEqual } from "node:crypto"; + +export const BASIC_AUTH_USERNAME = "submitqueue"; +export const BASIC_AUTH_REALM = "SubmitQueue demo"; +export const MINIMUM_TOKEN_LENGTH = 32; + +export type AuthConfiguration = Readonly<{ + token: string; +}>; + +export function loadAuthConfiguration( + environment: Readonly> = process.env, +): AuthConfiguration { + const token = environment.SUBMITQUEUE_WEB_TOKEN ?? ""; + if (token.length < MINIMUM_TOKEN_LENGTH) { + throw new Error( + `SUBMITQUEUE_WEB_TOKEN must contain at least ${MINIMUM_TOKEN_LENGTH} characters`, + ); + } + return { token }; +} + +function decodeBasicCredentials( + authorization: string | null, +): Readonly<{ username: string; password: string }> | undefined { + if (authorization === null) { + return undefined; + } + + const [scheme, encoded, ...extra] = authorization.trim().split(/\s+/u); + if (scheme?.toLowerCase() !== "basic" || !encoded || extra.length !== 0) { + return undefined; + } + if ( + !/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded) || + encoded.length % 4 === 1 + ) { + return undefined; + } + + const decoded = Buffer.from(encoded, "base64").toString("utf8"); + + const separator = decoded.indexOf(":"); + if (separator < 0) { + return undefined; + } + return { + username: decoded.slice(0, separator), + password: decoded.slice(separator + 1), + }; +} + +function digest(value: string): Buffer { + return createHash("sha256").update(value, "utf8").digest(); +} + +function securelyEqual(actual: string, expected: string): boolean { + return timingSafeEqual(digest(actual), digest(expected)); +} + +export function isAuthorized( + authorization: string | null, + configuration: AuthConfiguration, +): boolean { + const credentials = decodeBasicCredentials(authorization); + if (!credentials) { + return false; + } + return ( + securelyEqual(credentials.username, BASIC_AUTH_USERNAME) && + securelyEqual(credentials.password, configuration.token) + ); +} + +export function authenticationChallenge(): Response { + return new Response("Authentication required", { + status: 401, + headers: { + "Cache-Control": "no-store", + "Content-Type": "text/plain; charset=utf-8", + "WWW-Authenticate": `Basic realm="${BASIC_AUTH_REALM}", charset="UTF-8"`, + }, + }); +} diff --git a/web/service/submitqueue/src/server/config.test.ts b/web/service/submitqueue/src/server/config.test.ts new file mode 100644 index 000000000..7f384068c --- /dev/null +++ b/web/service/submitqueue/src/server/config.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { loadGatewayConfiguration } from "./config"; + +describe("loadGatewayConfiguration", () => { + it("uses TLS by default", () => { + expect( + loadGatewayConfiguration({ + SUBMITQUEUE_GATEWAY_URL: "https://gateway.example:8081", + }), + ).toEqual({ + baseUrl: "https://gateway.example:8081", + allowPlaintext: false, + }); + }); + + it("allows h2c only when explicitly enabled", () => { + expect( + loadGatewayConfiguration({ + SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT: "true", + SUBMITQUEUE_GATEWAY_URL: "http://gateway-service:8081", + }), + ).toEqual({ + baseUrl: "http://gateway-service:8081", + allowPlaintext: true, + }); + }); + + it.each([ + ["missing URL", {}], + ["relative URL", { SUBMITQUEUE_GATEWAY_URL: "gateway:8081" }], + ["implicit plaintext", { SUBMITQUEUE_GATEWAY_URL: "http://gateway:8081" }], + ["unsupported scheme", { SUBMITQUEUE_GATEWAY_URL: "ftp://gateway:8081" }], + ["path", { SUBMITQUEUE_GATEWAY_URL: "https://gateway:8081/grpc" }], + ["query", { SUBMITQUEUE_GATEWAY_URL: "https://gateway:8081?x=1" }], + ["credentials", { SUBMITQUEUE_GATEWAY_URL: "https://user:pass@gateway:8081" }], + ])("rejects %s", (_name, environment) => { + expect(() => loadGatewayConfiguration(environment)).toThrow(); + }); +}); diff --git a/web/service/submitqueue/src/server/config.ts b/web/service/submitqueue/src/server/config.ts new file mode 100644 index 000000000..7050231d4 --- /dev/null +++ b/web/service/submitqueue/src/server/config.ts @@ -0,0 +1,45 @@ +import "server-only"; + +export const DEMO_QUEUE = "demo-queue"; +export const GATEWAY_DEADLINE_MS = 5_000; + +export type GatewayConfiguration = Readonly<{ + baseUrl: string; + allowPlaintext: boolean; +}>; + +export function loadGatewayConfiguration( + environment: Readonly> = process.env, +): GatewayConfiguration { + const rawBaseUrl = environment.SUBMITQUEUE_GATEWAY_URL; + if (!rawBaseUrl) { + throw new Error("SUBMITQUEUE_GATEWAY_URL is required"); + } + + if (!URL.canParse(rawBaseUrl)) { + throw new Error("SUBMITQUEUE_GATEWAY_URL must be a valid absolute URL"); + } + const gatewayUrl = new URL(rawBaseUrl); + + if (gatewayUrl.pathname !== "/" || gatewayUrl.search || gatewayUrl.hash) { + throw new Error("SUBMITQUEUE_GATEWAY_URL must not contain a path, query, or fragment"); + } + if (gatewayUrl.username || gatewayUrl.password) { + throw new Error("SUBMITQUEUE_GATEWAY_URL must not contain credentials"); + } + + const allowPlaintext = environment.SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT === "true"; + if (gatewayUrl.protocol === "http:" && !allowPlaintext) { + throw new Error( + "Plaintext gateway connections require SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT=true", + ); + } + if (gatewayUrl.protocol !== "https:" && gatewayUrl.protocol !== "http:") { + throw new Error("SUBMITQUEUE_GATEWAY_URL must use https, or explicitly enabled http"); + } + + return { + baseUrl: gatewayUrl.origin, + allowPlaintext, + }; +} diff --git a/web/service/submitqueue/src/server/gateway.ts b/web/service/submitqueue/src/server/gateway.ts new file mode 100644 index 000000000..cece8a254 --- /dev/null +++ b/web/service/submitqueue/src/server/gateway.ts @@ -0,0 +1,28 @@ +import "server-only"; + +import { createClient } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { SubmitQueueGateway } from "@submitqueue/api/submitqueue/gateway"; +import type { + GatewayReader, + GatewayResolver, +} from "@submitqueue/web-submitqueue/server"; +import { gatewayReader } from "@submitqueue/web-submitqueue/server"; + +import { GATEWAY_DEADLINE_MS, loadGatewayConfiguration } from "./config"; + +let reader: GatewayReader | undefined; + +function createGatewayReader(): GatewayReader { + const configuration = loadGatewayConfiguration(); + const transport = createGrpcTransport({ + baseUrl: configuration.baseUrl, + defaultTimeoutMs: GATEWAY_DEADLINE_MS, + }); + return gatewayReader(createClient(SubmitQueueGateway, transport)); +} + +export const resolveDemoGateway: GatewayResolver = () => { + reader ??= createGatewayReader(); + return reader; +}; diff --git a/web/service/submitqueue/src/server/request-auth.ts b/web/service/submitqueue/src/server/request-auth.ts new file mode 100644 index 000000000..86dd45a29 --- /dev/null +++ b/web/service/submitqueue/src/server/request-auth.ts @@ -0,0 +1,18 @@ +import "server-only"; + +import { headers } from "next/headers"; +import { unauthorized } from "next/navigation"; + +import { isAuthorized, loadAuthConfiguration } from "./auth"; + +export async function requireAuthorization(): Promise { + const requestHeaders = await headers(); + if ( + !isAuthorized( + requestHeaders.get("authorization"), + loadAuthConfiguration(), + ) + ) { + unauthorized(); + } +} diff --git a/web/service/submitqueue/src/server/window.test.ts b/web/service/submitqueue/src/server/window.test.ts new file mode 100644 index 000000000..70beb589f --- /dev/null +++ b/web/service/submitqueue/src/server/window.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { + defaultRequestWindow, + parseRequestWindow, + REQUEST_WINDOW_MS, + requestWindowSearch, +} from "./window"; + +describe("request window", () => { + it("creates a fixed trailing 24-hour interval", () => { + expect(defaultRequestWindow(REQUEST_WINDOW_MS + 100)).toEqual({ + fromMs: 100, + toMs: REQUEST_WINDOW_MS + 100, + }); + }); + + it("parses stable bounds and an opaque page token", () => { + expect( + parseRequestWindow({ from: "100", to: "200", page: "token/+=" }), + ).toEqual({ fromMs: 100, toMs: 200, pageToken: "token/+=" }); + }); + + it.each([ + ["missing bounds", {}], + ["duplicate bounds", { from: ["1", "2"], to: "3" }], + ["negative bound", { from: "-1", to: "3" }], + ["fractional bound", { from: "1.5", to: "3" }], + ["unsafe bound", { from: "1", to: "9007199254740992" }], + ["empty interval", { from: "3", to: "3" }], + ["reversed interval", { from: "4", to: "3" }], + ])("rejects %s", (_name, search) => { + expect(parseRequestWindow(search)).toBeUndefined(); + }); + + it("serializes bounds and preserves the page token", () => { + expect( + requestWindowSearch({ fromMs: 100, toMs: 200, pageToken: "a/b+c" }).toString(), + ).toBe("from=100&to=200&page=a%2Fb%2Bc"); + }); +}); diff --git a/web/service/submitqueue/src/server/window.ts b/web/service/submitqueue/src/server/window.ts new file mode 100644 index 000000000..804ffdaf1 --- /dev/null +++ b/web/service/submitqueue/src/server/window.ts @@ -0,0 +1,54 @@ +export const REQUEST_WINDOW_MS = 24 * 60 * 60 * 1_000; +export const REQUEST_PAGE_SIZE = 50; + +export type RequestWindow = Readonly<{ + fromMs: number; + toMs: number; + pageToken?: string; +}>; + +export type RequestSearchParams = Readonly<{ + from?: string | string[]; + to?: string | string[]; + page?: string | string[]; +}>; + +function one(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? undefined : value; +} + +function parseMillisecond(value: string | undefined): number | undefined { + if (!value || !/^\d+$/u.test(value)) { + return undefined; + } + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined; +} + +export function parseRequestWindow( + searchParams: RequestSearchParams, +): RequestWindow | undefined { + const fromMs = parseMillisecond(one(searchParams.from)); + const toMs = parseMillisecond(one(searchParams.to)); + if (fromMs === undefined || toMs === undefined || fromMs >= toMs) { + return undefined; + } + + const pageToken = one(searchParams.page); + return pageToken ? { fromMs, toMs, pageToken } : { fromMs, toMs }; +} + +export function defaultRequestWindow(nowMs = Date.now()): RequestWindow { + return { fromMs: nowMs - REQUEST_WINDOW_MS, toMs: nowMs }; +} + +export function requestWindowSearch(window: RequestWindow): URLSearchParams { + const search = new URLSearchParams({ + from: String(window.fromMs), + to: String(window.toMs), + }); + if (window.pageToken) { + search.set("page", window.pageToken); + } + return search; +} diff --git a/web/service/submitqueue/src/test/server-only.ts b/web/service/submitqueue/src/test/server-only.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/web/service/submitqueue/src/test/server-only.ts @@ -0,0 +1 @@ +export {}; diff --git a/web/service/submitqueue/tsconfig.json b/web/service/submitqueue/tsconfig.json new file mode 100644 index 000000000..2523b07a6 --- /dev/null +++ b/web/service/submitqueue/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "allowJs": false, + "esModuleInterop": true, + "incremental": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "module": "esnext", + "moduleResolution": "bundler", + "noEmit": true, + "plugins": [ + { + "name": "next" + } + ], + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/web/service/submitqueue/vitest.config.ts b/web/service/submitqueue/vitest.config.ts new file mode 100644 index 000000000..4da82d11b --- /dev/null +++ b/web/service/submitqueue/vitest.config.ts @@ -0,0 +1,13 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "server-only": path.resolve(import.meta.dirname, "src/test/server-only.ts"), + }, + }, + test: { + environment: "node", + }, +}); diff --git a/web/submitqueue/.gitignore b/web/submitqueue/.gitignore new file mode 100644 index 000000000..1eae0cf67 --- /dev/null +++ b/web/submitqueue/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/web/submitqueue/package.json b/web/submitqueue/package.json new file mode 100644 index 000000000..fa0f4fdf9 --- /dev/null +++ b/web/submitqueue/package.json @@ -0,0 +1,53 @@ +{ + "name": "@submitqueue/web-submitqueue", + "version": "0.0.0", + "private": true, + "type": "module", + "sideEffects": false, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server.d.ts", + "import": "./dist/server.js" + }, + "./testing": { + "types": "./dist/testing.d.ts", + "import": "./dist/testing.js" + } + }, + "scripts": { + "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json", + "lint": "tsc --noEmit", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@submitqueue/api": "workspace:*" + }, + "peerDependencies": { + "@connectrpc/connect": ">=2 <3", + "@bufbuild/protobuf": ">=2 <3", + "next": ">=16.3 <17", + "react": ">=19 <20", + "react-dom": ">=19 <20" + }, + "devDependencies": { + "@connectrpc/connect": "^2.1.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "jsdom": "^26.1.0", + "next": "^16.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "typescript": "^5.8.3", + "vitest": "^3.1.2" + } +} diff --git a/web/submitqueue/src/auto-refresh.test.tsx b/web/submitqueue/src/auto-refresh.test.tsx new file mode 100644 index 000000000..ee759d35b --- /dev/null +++ b/web/submitqueue/src/auto-refresh.test.tsx @@ -0,0 +1,87 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const routerRefresh = vi.fn(); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: routerRefresh }), +})); + +import { AutoRefresh } from "./auto-refresh"; + +describe("AutoRefresh", () => { + beforeEach(() => { + vi.useFakeTimers(); + routerRefresh.mockReset(); + Object.defineProperty(navigator, "onLine", { configurable: true, value: true }); + Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" }); + }); + + afterEach(() => vi.useRealTimers()); + + it("uses the interval plus jitter and continues polling", async () => { + const refresh = vi.fn(async () => undefined); + render( 0.5} refresh={refresh} />); + + await act(async () => vi.advanceTimersByTime(2_249)); + expect(refresh).not.toHaveBeenCalled(); + await act(async () => vi.advanceTimersByTime(1)); + expect(refresh).toHaveBeenCalledTimes(1); + await act(async () => vi.advanceTimersByTime(2_250)); + expect(refresh).toHaveBeenCalledTimes(2); + }); + + it("keeps manual and timer refreshes single-flight", async () => { + let finish: (() => void) | undefined; + const refresh = vi.fn(() => new Promise((resolve) => (finish = resolve))); + render(); + + await act(async () => vi.advanceTimersByTime(100)); + fireEvent.click(screen.getByRole("button", { name: "Refreshing…" })); + expect(refresh).toHaveBeenCalledTimes(1); + await act(async () => finish?.()); + }); + + it("backs off transient failures up to the configured cap", async () => { + const refresh = vi.fn(async () => undefined); + render( + 0} + refresh={refresh} + transientFailureCount={4} + />, + ); + + await act(async () => vi.advanceTimersByTime(29_999)); + expect(refresh).not.toHaveBeenCalled(); + await act(async () => vi.advanceTimersByTime(1)); + expect(refresh).toHaveBeenCalledTimes(1); + }); + + it("pauses while offline and resumes when online", async () => { + Object.defineProperty(navigator, "onLine", { configurable: true, value: false }); + const refresh = vi.fn(async () => undefined); + render(); + + expect(screen.getByText("Automatic refresh paused")).toBeTruthy(); + await act(async () => vi.advanceTimersByTime(1_000)); + expect(refresh).not.toHaveBeenCalled(); + Object.defineProperty(navigator, "onLine", { configurable: true, value: true }); + fireEvent(window, new Event("online")); + await act(async () => vi.advanceTimersByTime(100)); + expect(refresh).toHaveBeenCalledTimes(1); + }); + + it("stops automatic polling for terminal detail views", async () => { + const refresh = vi.fn(async () => undefined); + render(); + + await act(async () => vi.advanceTimersByTime(1_000)); + expect(refresh).not.toHaveBeenCalled(); + expect(screen.getByText("Automatic refresh stopped")).toBeTruthy(); + await act(async () => fireEvent.click(screen.getByRole("button", { name: "Refresh" }))); + expect(refresh).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web/submitqueue/src/auto-refresh.tsx b/web/submitqueue/src/auto-refresh.tsx new file mode 100644 index 000000000..86b3dae1a --- /dev/null +++ b/web/submitqueue/src/auto-refresh.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; + +export interface AutoRefreshProps { + terminal?: boolean; + intervalMs?: number; + jitterMs?: number; + maxBackoffMs?: number; + transientFailureCount?: number; + refresh?: () => void | Promise; + random?: () => number; +} + +function pageCanRefresh(): boolean { + return document.visibilityState !== "hidden" && navigator.onLine; +} + +export function AutoRefresh({ + terminal = false, + intervalMs = 2_000, + jitterMs = 500, + maxBackoffMs = 30_000, + transientFailureCount = 0, + refresh, + random = Math.random, +}: AutoRefreshProps) { + const router = useRouter(); + const [refreshing, setRefreshing] = useState(false); + const [paused, setPaused] = useState(false); + const [cycle, setCycle] = useState(0); + const inFlight = useRef(false); + const localFailures = useRef(0); + const timer = useRef | null>(null); + const routerRefresh = useCallback(() => router.refresh(), [router]); + const refreshPage = refresh ?? routerRefresh; + + const runRefresh = useCallback(async () => { + if (inFlight.current || !pageCanRefresh()) { + setPaused(!pageCanRefresh()); + return; + } + inFlight.current = true; + setRefreshing(true); + try { + await refreshPage(); + localFailures.current = 0; + } catch { + localFailures.current += 1; + } finally { + inFlight.current = false; + setRefreshing(false); + setCycle((value) => value + 1); + } + }, [refreshPage]); + + useEffect(() => { + const updateAvailability = () => setPaused(!pageCanRefresh()); + document.addEventListener("visibilitychange", updateAvailability); + window.addEventListener("online", updateAvailability); + window.addEventListener("offline", updateAvailability); + updateAvailability(); + return () => { + document.removeEventListener("visibilitychange", updateAvailability); + window.removeEventListener("online", updateAvailability); + window.removeEventListener("offline", updateAvailability); + }; + }, []); + + useEffect(() => { + if (terminal || paused) { + return; + } + const failures = Math.max(transientFailureCount, localFailures.current); + const multiplier = 2 ** Math.min(failures, 20); + const backoff = Math.min(intervalMs * multiplier, maxBackoffMs); + const delay = backoff + Math.floor(random() * (jitterMs + 1)); + timer.current = setTimeout(() => { + void runRefresh(); + }, delay); + return () => { + if (timer.current !== null) { + clearTimeout(timer.current); + timer.current = null; + } + }; + }, [cycle, intervalMs, jitterMs, maxBackoffMs, paused, random, runRefresh, terminal, transientFailureCount]); + + return ( +
+ + + {terminal ? "Automatic refresh stopped" : paused ? "Automatic refresh paused" : ""} + +
+ ); +} diff --git a/web/submitqueue/src/components.test.tsx b/web/submitqueue/src/components.test.tsx new file mode 100644 index 000000000..9f40fbde7 --- /dev/null +++ b/web/submitqueue/src/components.test.tsx @@ -0,0 +1,100 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ErrorState, RequestDetail, RequestList, RequestStatus } from "./components"; +import type { RequestDetailModel, RequestListModel } from "./models"; + +const request = { + sqid: "demo-queue/1", + queue: "demo-queue", + changeUris: ["github://github.com/uber/submitqueue/pull/1/abc"], + receivedAtMs: 1_700_000_000_000, + status: "speculating", + lastError: null, + metadata: { batch: "batch-1" }, +}; + +describe("request components", () => { + it("renders a linked queue request with an encoded sqid", () => { + const model: RequestListModel = { + queue: "demo-queue", + receivedAtOrAfterMs: 1, + receivedBeforeMs: 2, + requests: [request], + nextPageToken: "opaque-token", + }; + render(); + + const link = screen.getByRole("link", { name: "demo-queue/1" }); + expect(link.getAttribute("href")).toBe("/requests/ZGVtby1xdWV1ZS8x"); + expect(screen.getByText("Speculating").getAttribute("data-tone")).toBe("progress"); + expect(screen.getByRole("link", { name: "Next page" }).getAttribute("href")).toBe( + "/requests?from=1&to=2&page=opaque-token", + ); + }); + + it("renders an accessible empty state", () => { + render( + , + ); + expect(screen.getByText("No requests were received in this window.")).toBeTruthy(); + }); + + it("renders summary and ordered status/build history together", () => { + const model: RequestDetailModel = { + request: { ...request, status: "landed" }, + history: [ + { + timestampMs: 1_700_000_000_000, + type: "status", + status: "started", + event: null, + lastError: null, + metadata: {}, + }, + { + timestampMs: 1_700_000_001_000, + type: "event", + status: null, + event: "building", + lastError: null, + metadata: { build_url: "https://build.example/1" }, + }, + ], + historyError: null, + }; + render(); + + expect(screen.getByRole("heading", { level: 1 }).textContent).toContain("demo-queue/1"); + const history = screen.getByRole("list", { name: "Request history" }); + expect(within(history).getAllByRole("listitem")).toHaveLength(2); + expect(within(history).getByText("Building")).toBeTruthy(); + expect(within(history).getByText("https://build.example/1")).toBeTruthy(); + }); + + it("shows unknown statuses safely", () => { + render(); + expect(screen.getByText("New Pipeline Step").getAttribute("data-tone")).toBe("neutral"); + }); + + it("uses an alert for a full-page safe error", () => { + render( + , + ); + expect(screen.getByRole("alert").textContent).toContain("Something went wrong"); + }); +}); diff --git a/web/submitqueue/src/components.tsx b/web/submitqueue/src/components.tsx new file mode 100644 index 000000000..7bd3e1ee1 --- /dev/null +++ b/web/submitqueue/src/components.tsx @@ -0,0 +1,180 @@ +"use client"; + +import type { ReactNode } from "react"; +import type { + HistoryEventModel, + RequestDetailModel, + RequestListModel, + RequestSummaryModel, + WebError, +} from "./models.js"; +import { WebPaths } from "./paths.js"; +import { statusDisplay } from "./status.js"; + +function Timestamp({ value }: { value: number }) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return ; + } + return ; +} + +function Metadata({ values }: { values: Record }) { + const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)); + if (entries.length === 0) { + return null; + } + return ( +
+ {entries.map(([key, value]) => ( +
+
{key}
+
{value}
+
+ ))} +
+ ); +} + +export function RequestStatus({ status }: { status: string }) { + const display = statusDisplay(status); + return ( + + {display.label} + + ); +} + +function RequestChanges({ request }: { request: RequestSummaryModel }) { + return ( +
    + {request.changeUris.map((uri) => ( +
  • + {uri} +
  • + ))} +
+ ); +} + +export interface RequestListProps { + model: RequestListModel; + requestsBasePath?: string; + empty?: ReactNode; +} + +export function RequestList({ model, requestsBasePath, empty }: RequestListProps) { + if (model.requests.length === 0) { + return
{empty ?? "No requests were received in this window."}
; + } + const paths = new WebPaths( + requestsBasePath === undefined ? {} : { requestsPath: requestsBasePath }, + ); + return ( + <> +
    + {model.requests.map((request) => ( +
  1. +
    +
    + {request.sqid} + +
    + +

    + Received +

    + {request.lastError ?

    {request.lastError}

    : null} +
    +
  2. + ))} +
+ {model.nextPageToken ? ( + + ) : null} + + ); +} + +function HistoryValue({ item }: { item: HistoryEventModel }) { + if (item.type === "status" && item.status !== null) { + return ; + } + if (item.type === "event" && item.event !== null) { + return {statusDisplay(item.event).label}; + } + return Unknown event; +} + +export function RequestHistory({ events }: { events: HistoryEventModel[] }) { + if (events.length === 0) { + return

No history has been retained for this request.

; + } + return ( +
    + {events.map((item, index) => ( +
  1. + + + {item.lastError ?

    {item.lastError}

    : null} + +
  2. + ))} +
+ ); +} + +export function ErrorState({ error, compact = false }: { error: WebError; compact?: boolean }) { + return ( +
+

{error.title}

+

{error.message}

+
+ ); +} + +export interface RequestDetailProps { + model: RequestDetailModel; +} + +export function RequestDetail({ model }: RequestDetailProps) { + const { request } = model; + return ( +
+
+

{request.queue}

+

{request.sqid}

+ +

+ Received +

+
+
+

Changes

+ +
+ {request.lastError ? ( +
+

Last error

+

{request.lastError}

+
+ ) : null} + +
+

History

+ {model.historyError ? : null} + +
+
+ ); +} diff --git a/web/submitqueue/src/index.ts b/web/submitqueue/src/index.ts new file mode 100644 index 000000000..a36a9e4ca --- /dev/null +++ b/web/submitqueue/src/index.ts @@ -0,0 +1,32 @@ +"use client"; + +export { AutoRefresh, type AutoRefreshProps } from "./auto-refresh.js"; +export { + ErrorState, + RequestDetail, + type RequestDetailProps, + RequestHistory, + RequestList, + type RequestListProps, + RequestStatus, +} from "./components.js"; +export type { + HistoryEventModel, + HistoryEventType, + LoadResult, + RequestDetailModel, + RequestListModel, + RequestSummaryModel, + StatusDisplay, + StatusTone, + WebError, + WebErrorKind, +} from "./models.js"; +export { + decodeSqidSegment, + encodeSqidSegment, + type RequestsPathOptions, + WebPaths, + type WebPathsOptions, +} from "./paths.js"; +export { isTerminalStatus, knownRequestStatuses, statusDisplay } from "./status.js"; diff --git a/web/submitqueue/src/models.ts b/web/submitqueue/src/models.ts new file mode 100644 index 000000000..556912ae4 --- /dev/null +++ b/web/submitqueue/src/models.ts @@ -0,0 +1,55 @@ +export type StatusTone = "neutral" | "progress" | "success" | "danger" | "warning"; + +export interface StatusDisplay { + label: string; + tone: StatusTone; + terminal: boolean; +} + +export interface RequestSummaryModel { + sqid: string; + queue: string; + changeUris: string[]; + receivedAtMs: number; + status: string; + lastError: string | null; + metadata: Record; +} + +export type HistoryEventType = "status" | "event" | "unknown"; + +export interface HistoryEventModel { + timestampMs: number; + type: HistoryEventType; + status: string | null; + event: string | null; + lastError: string | null; + metadata: Record; +} + +export interface RequestListModel { + queue: string; + receivedAtOrAfterMs: number; + receivedBeforeMs: number; + requests: RequestSummaryModel[]; + nextPageToken: string | null; +} + +export interface RequestDetailModel { + request: RequestSummaryModel; + history: HistoryEventModel[]; + historyError: WebError | null; +} + +export type WebErrorKind = "user" | "not-found" | "transient" | "internal"; + +export interface WebError { + kind: WebErrorKind; + title: string; + message: string; + retryable: boolean; +} + +export type LoadResult = + | { ok: true; data: T } + | { ok: false; error: WebError }; diff --git a/web/submitqueue/src/paths.test.ts b/web/submitqueue/src/paths.test.ts new file mode 100644 index 000000000..1ffbbbd02 --- /dev/null +++ b/web/submitqueue/src/paths.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { decodeSqidSegment, encodeSqidSegment, WebPaths } from "./paths"; + +describe("WebPaths", () => { + it("round-trips a slash-containing sqid in one path segment", () => { + const paths = new WebPaths(); + const href = paths.request("demo-queue/42"); + const segment = href.split("/").at(-1); + + expect(segment).toBeDefined(); + expect(segment).not.toContain("/"); + expect(paths.decodeRequest(segment!)).toBe("demo-queue/42"); + expect(encodeSqidSegment("demo-queue/42")).not.toMatch(/=|\//u); + }); + + it("rejects malformed segments without throwing", () => { + expect(decodeSqidSegment("not+base64")).toBeUndefined(); + expect(decodeSqidSegment("_w")).toBeUndefined(); + }); + + it("preserves fixed list bounds while paging", () => { + const paths = new WebPaths({ requestsPath: "/submitqueue/requests/" }); + const href = paths.requests({ fromMs: 10, toMs: 20, pageToken: "opaque+/=" }); + + expect(href).toBe("/submitqueue/requests?from=10&to=20&page=opaque%2B%2F%3D"); + }); +}); diff --git a/web/submitqueue/src/paths.ts b/web/submitqueue/src/paths.ts new file mode 100644 index 000000000..fc760fbb3 --- /dev/null +++ b/web/submitqueue/src/paths.ts @@ -0,0 +1,79 @@ +export interface RequestsPathOptions { + fromMs?: number; + toMs?: number; + pageToken?: string | null; +} + +export interface WebPathsOptions { + requestsPath?: string; +} + +function encodeBase64Url(value: string): string { + const bytes = new TextEncoder().encode(value); + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return globalThis + .btoa(binary) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replace(/=+$/u, ""); +} + +function decodeBase64Url(value: string): string | undefined { + if (!/^[A-Za-z0-9_-]+$/u.test(value)) { + return undefined; + } + const standard = value.replaceAll("-", "+").replaceAll("_", "/"); + const padding = "=".repeat((4 - (standard.length % 4)) % 4); + try { + const binary = globalThis.atob(`${standard}${padding}`); + const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + return undefined; + } +} + +export class WebPaths { + readonly requestsPath: string; + + constructor(options: WebPathsOptions = {}) { + const requestedPath = options.requestsPath ?? "/requests"; + this.requestsPath = requestedPath.endsWith("/") + ? requestedPath.slice(0, -1) + : requestedPath; + } + + requests(options: RequestsPathOptions = {}): string { + const parameters = new URLSearchParams(); + if (options.fromMs !== undefined) { + parameters.set("from", String(options.fromMs)); + } + if (options.toMs !== undefined) { + parameters.set("to", String(options.toMs)); + } + if (options.pageToken) { + parameters.set("page", options.pageToken); + } + const query = parameters.toString(); + return query === "" ? this.requestsPath : `${this.requestsPath}?${query}`; + } + + request(sqid: string): string { + return `${this.requestsPath}/${encodeBase64Url(sqid)}`; + } + + decodeRequest(segment: string): string | undefined { + return decodeBase64Url(segment); + } +} + +export function encodeSqidSegment(sqid: string): string { + return encodeBase64Url(sqid); +} + +export function decodeSqidSegment(segment: string): string | undefined { + return decodeBase64Url(segment); +} diff --git a/web/submitqueue/src/server.test.ts b/web/submitqueue/src/server.test.ts new file mode 100644 index 000000000..29cf75a0f --- /dev/null +++ b/web/submitqueue/src/server.test.ts @@ -0,0 +1,129 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createFakeGatewayReader, gatewayHistoryFixture, gatewayRequestFixture } from "./testing"; + +vi.mock("next/server", () => ({ connection: vi.fn(async () => undefined) })); + +import { + classifyGatewayError, + loadRequestDetail, + loadRequestList, + safeInt64ToNumber, +} from "./server"; + +describe("server presentation mapping", () => { + beforeEach(() => vi.clearAllMocks()); + + it("converts int64 only within JavaScript's safe range", () => { + expect(safeInt64ToNumber(1_700_000_000_000n)).toBe(1_700_000_000_000); + expect(safeInt64ToNumber(BigInt(Number.MAX_SAFE_INTEGER) + 1n)).toBeNull(); + expect(safeInt64ToNumber("not-an-integer")).toBeNull(); + }); + + it("maps a request page while preserving its bounds", async () => { + const reader = createFakeGatewayReader({ nextPageToken: "next" }); + const result = await loadRequestList(() => reader, { + queue: "demo-queue", + receivedAtOrAfterMs: 100, + receivedBeforeMs: 200, + pageToken: "current", + }); + + expect(result).toMatchObject({ + ok: true, + data: { + queue: "demo-queue", + receivedAtOrAfterMs: 100, + receivedBeforeMs: 200, + nextPageToken: "next", + requests: [{ sqid: "demo-queue/1", receivedAtMs: 1_700_000_000_000 }], + }, + }); + }); + + it("returns an explicit invalid-input result before resolving a client", async () => { + const resolver = vi.fn(() => createFakeGatewayReader()); + const result = await loadRequestList(resolver, { + queue: "demo-queue", + receivedAtOrAfterMs: 200, + receivedBeforeMs: 100, + }); + + expect(result).toMatchObject({ ok: false, error: { kind: "user" } }); + expect(resolver).not.toHaveBeenCalled(); + }); + + it("keeps the summary available when history has a transient failure", async () => { + const reader = createFakeGatewayReader({ + summary: gatewayRequestFixture({ status: "landed" }), + historyError: new ConnectError("private upstream text", Code.Unavailable), + }); + const result = await loadRequestDetail(() => reader, { + queue: "demo-queue", + sqid: "demo-queue/1", + }); + + expect(result).toMatchObject({ + ok: true, + data: { + request: { status: "landed" }, + history: [], + historyError: { kind: "transient", retryable: true }, + }, + }); + expect(JSON.stringify(result)).not.toContain("private upstream text"); + }); + + it("maps chronological status and occurrence history", async () => { + const reader = createFakeGatewayReader({ + history: [ + gatewayHistoryFixture({ timestampMs: 1n, status: "started" }), + gatewayHistoryFixture({ + timestampMs: 2n, + type: "event", + status: "", + event: "building", + metadata: { build_url: "https://build.example/1" }, + }), + ], + }); + const result = await loadRequestDetail(() => reader, { + queue: "demo-queue", + sqid: "demo-queue/1", + }); + + expect(result).toMatchObject({ + ok: true, + data: { + history: [ + { timestampMs: 1, type: "status", status: "started" }, + { timestampMs: 2, type: "event", event: "building" }, + ], + historyError: null, + }, + }); + }); +}); + +describe("gateway error classification", () => { + it.each([ + [Code.InvalidArgument, "user", false], + [Code.NotFound, "not-found", false], + [Code.ResourceExhausted, "user", false], + [Code.Unavailable, "transient", true], + [Code.DeadlineExceeded, "transient", true], + [Code.Internal, "internal", false], + ] as const)("classifies code %s", (code, kind, retryable) => { + const classified = classifyGatewayError(new ConnectError("raw gateway detail", code)); + + expect(classified).toMatchObject({ kind, retryable }); + expect(classified.message).not.toContain("raw gateway detail"); + }); + + it("does not expose arbitrary errors", () => { + expect(classifyGatewayError(new Error("database password"))).toMatchObject({ + kind: "internal", + retryable: false, + }); + }); +}); diff --git a/web/submitqueue/src/server.ts b/web/submitqueue/src/server.ts new file mode 100644 index 000000000..22f859c6a --- /dev/null +++ b/web/submitqueue/src/server.ts @@ -0,0 +1,293 @@ +import { Code, ConnectError, type Client } from "@connectrpc/connect"; +import { SubmitQueueGateway } from "@submitqueue/api/submitqueue/gateway"; +import { connection } from "next/server"; +import type { + HistoryEventModel, + LoadResult, + RequestDetailModel, + RequestListModel, + RequestSummaryModel, + WebError, +} from "./models.js"; + +export { decodeSqidSegment, encodeSqidSegment, WebPaths } from "./paths.js"; +export { isTerminalStatus, statusDisplay } from "./status.js"; + +type Int64Value = bigint | number | string; +const GATEWAY_TIMEOUT_MS = 5_000; + +export interface GatewayCallOptions { + timeoutMs?: number; +} + +export interface GatewayRequestSummary { + sqid: string; + queue: string; + changeUris: readonly string[]; + receivedAtMs: Int64Value; + status: string; + lastError: string; + metadata: Readonly>; +} + +export interface GatewayHistoryEvent { + timestampMs: Int64Value; + status: string; + lastError: string; + metadata: Readonly>; + type: string; + event: string; +} + +export interface GatewayReader { + list( + input: { + queue: string; + receivedAtOrAfterMs: bigint; + receivedBeforeMs: bigint; + pageSize: number; + pageToken: string; + }, + options?: GatewayCallOptions, + ): Promise<{ + requests: readonly GatewayRequestSummary[]; + nextPageToken: string; + }>; + getRequestSummaryByID( + input: { sqid: string; queue: string }, + options?: GatewayCallOptions, + ): Promise<{ + request?: GatewayRequestSummary | undefined; + }>; + getRequestHistoryByID( + input: { sqid: string; queue: string }, + options?: GatewayCallOptions, + ): Promise<{ + events: readonly GatewayHistoryEvent[]; + }>; +} + +export type GatewayResolver = (queue: string) => GatewayReader; + +export type SubmitQueueGatewayClient = Client; + +export function gatewayReader(client: SubmitQueueGatewayClient): GatewayReader { + return client; +} + +export interface LoadRequestListInput { + queue: string; + receivedAtOrAfterMs: number; + receivedBeforeMs: number; + pageSize?: number; + pageToken?: string; +} + +export interface LoadRequestDetailInput { + queue: string; + sqid: string; +} + +const INVALID_INPUT: WebError = { + kind: "user", + title: "Invalid request", + message: "The request parameters are invalid.", + retryable: false, +}; + +const INTERNAL_ERROR: WebError = { + kind: "internal", + title: "Something went wrong", + message: "SubmitQueue could not load this information.", + retryable: false, +}; + +export function safeInt64ToNumber(value: Int64Value): number | null { + let converted: bigint; + if (typeof value === "bigint") { + converted = value; + } else if (typeof value === "number") { + return Number.isSafeInteger(value) ? value : null; + } else { + try { + converted = BigInt(value); + } catch { + return null; + } + } + if (converted > BigInt(Number.MAX_SAFE_INTEGER) || converted < BigInt(Number.MIN_SAFE_INTEGER)) { + return null; + } + return Number(converted); +} + +export function classifyGatewayError(error: unknown): WebError { + if (!(error instanceof ConnectError)) { + return INTERNAL_ERROR; + } + switch (error.code) { + case Code.InvalidArgument: + return INVALID_INPUT; + case Code.NotFound: + return { + kind: "not-found", + title: "Request not found", + message: "The requested SubmitQueue request was not found.", + retryable: false, + }; + case Code.ResourceExhausted: + return { + kind: "user", + title: "Request limit reached", + message: "The requested result set is too large. Narrow the time window and try again.", + retryable: false, + }; + case Code.Unavailable: + case Code.DeadlineExceeded: + return { + kind: "transient", + title: "SubmitQueue is unavailable", + message: "SubmitQueue could not be reached. Try again shortly.", + retryable: true, + }; + default: + return INTERNAL_ERROR; + } +} + +function mapSummary(value: GatewayRequestSummary): RequestSummaryModel | null { + const receivedAtMs = safeInt64ToNumber(value.receivedAtMs); + if (receivedAtMs === null) { + return null; + } + return { + sqid: value.sqid, + queue: value.queue, + changeUris: [...value.changeUris], + receivedAtMs, + status: value.status, + lastError: value.lastError === "" ? null : value.lastError, + metadata: { ...value.metadata }, + }; +} + +function mapHistoryEvent(value: GatewayHistoryEvent): HistoryEventModel | null { + const timestampMs = safeInt64ToNumber(value.timestampMs); + if (timestampMs === null) { + return null; + } + const type = value.type === "status" || value.type === "event" ? value.type : "unknown"; + return { + timestampMs, + type, + status: value.status === "" ? null : value.status, + event: value.event === "" ? null : value.event, + lastError: value.lastError === "" ? null : value.lastError, + metadata: { ...value.metadata }, + }; +} + +function validListInput(input: LoadRequestListInput): boolean { + const pageSize = input.pageSize ?? 50; + return ( + input.queue !== "" && + Number.isSafeInteger(input.receivedAtOrAfterMs) && + Number.isSafeInteger(input.receivedBeforeMs) && + input.receivedAtOrAfterMs < input.receivedBeforeMs && + Number.isInteger(pageSize) && + pageSize > 0 + ); +} + +export async function loadRequestList( + resolveGateway: GatewayResolver, + input: LoadRequestListInput, +): Promise> { + if (!validListInput(input)) { + return { ok: false, error: INVALID_INPUT }; + } + try { + await connection(); + const response = await resolveGateway(input.queue).list( + { + queue: input.queue, + receivedAtOrAfterMs: BigInt(input.receivedAtOrAfterMs), + receivedBeforeMs: BigInt(input.receivedBeforeMs), + pageSize: input.pageSize ?? 50, + pageToken: input.pageToken ?? "", + }, + { timeoutMs: GATEWAY_TIMEOUT_MS }, + ); + const requests: RequestSummaryModel[] = []; + for (const value of response.requests) { + const request = mapSummary(value); + if (request === null) { + return { ok: false, error: INTERNAL_ERROR }; + } + requests.push(request); + } + return { + ok: true, + data: { + queue: input.queue, + receivedAtOrAfterMs: input.receivedAtOrAfterMs, + receivedBeforeMs: input.receivedBeforeMs, + requests, + nextPageToken: response.nextPageToken === "" ? null : response.nextPageToken, + }, + }; + } catch (error) { + return { ok: false, error: classifyGatewayError(error) }; + } +} + +export async function loadRequestDetail( + resolveGateway: GatewayResolver, + input: LoadRequestDetailInput, +): Promise> { + if (input.queue === "" || input.sqid === "") { + return { ok: false, error: INVALID_INPUT }; + } + try { + await connection(); + const gateway = resolveGateway(input.queue); + const [summaryResult, historyResult] = await Promise.allSettled([ + gateway.getRequestSummaryByID(input, { timeoutMs: GATEWAY_TIMEOUT_MS }), + gateway.getRequestHistoryByID(input, { timeoutMs: GATEWAY_TIMEOUT_MS }), + ]); + if (summaryResult.status === "rejected") { + return { ok: false, error: classifyGatewayError(summaryResult.reason) }; + } + if (summaryResult.value.request === undefined) { + return { ok: false, error: INTERNAL_ERROR }; + } + const request = mapSummary(summaryResult.value.request); + if (request === null) { + return { ok: false, error: INTERNAL_ERROR }; + } + if (historyResult.status === "rejected") { + return { + ok: true, + data: { + request, + history: [], + historyError: classifyGatewayError(historyResult.reason), + }, + }; + } + const history: HistoryEventModel[] = []; + for (const value of historyResult.value.events) { + const event = mapHistoryEvent(value); + if (event === null) { + return { + ok: true, + data: { request, history: [], historyError: INTERNAL_ERROR }, + }; + } + history.push(event); + } + return { ok: true, data: { request, history, historyError: null } }; + } catch (error) { + return { ok: false, error: classifyGatewayError(error) }; + } +} diff --git a/web/submitqueue/src/status.test.ts b/web/submitqueue/src/status.test.ts new file mode 100644 index 000000000..8c75e2560 --- /dev/null +++ b/web/submitqueue/src/status.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { isTerminalStatus, knownRequestStatuses, statusDisplay } from "./status"; + +describe("request status display", () => { + it.each(["landed", "error", "cancelled"])("marks %s terminal", (status) => { + expect(isTerminalStatus(status)).toBe(true); + }); + + it.each(["accepted", "validating", "speculating", "landing", "cancelling"])( + "keeps %s active", + (status) => { + expect(isTerminalStatus(status)).toBe(false); + }, + ); + + it("renders newly-added statuses with a readable fallback", () => { + expect(statusDisplay("waiting_for_owner")).toEqual({ + label: "Waiting For Owner", + tone: "neutral", + terminal: false, + }); + expect(knownRequestStatuses).toContain("landed"); + }); +}); diff --git a/web/submitqueue/src/status.ts b/web/submitqueue/src/status.ts new file mode 100644 index 000000000..4de73d99b --- /dev/null +++ b/web/submitqueue/src/status.ts @@ -0,0 +1,49 @@ +import type { StatusDisplay } from "./models.js"; + +const STATUS_DISPLAY: Readonly> = { + accepting: { label: "Accepting", tone: "progress", terminal: false }, + accepted: { label: "Accepted", tone: "progress", terminal: false }, + started: { label: "Started", tone: "progress", terminal: false }, + validating: { label: "Validating", tone: "progress", terminal: false }, + validated: { label: "Validated", tone: "progress", terminal: false }, + batching: { label: "Batching", tone: "progress", terminal: false }, + batched: { label: "Batched", tone: "progress", terminal: false }, + speculating: { label: "Speculating", tone: "progress", terminal: false }, + speculated: { label: "Speculated", tone: "progress", terminal: false }, + landing: { label: "Landing", tone: "progress", terminal: false }, + landed: { label: "Landed", tone: "success", terminal: true }, + cancelling: { label: "Cancelling", tone: "warning", terminal: false }, + cancelled: { label: "Cancelled", tone: "neutral", terminal: true }, + error: { label: "Error", tone: "danger", terminal: true }, +}; + +const UNKNOWN_STATUS: StatusDisplay = { + label: "Unknown", + tone: "neutral", + terminal: false, +}; + +export const knownRequestStatuses = Object.freeze(Object.keys(STATUS_DISPLAY)); + +export function statusDisplay(status: string): StatusDisplay { + const known = STATUS_DISPLAY[status]; + if (known !== undefined) { + return known; + } + const normalized = status.trim(); + if (normalized === "") { + return UNKNOWN_STATUS; + } + return { + ...UNKNOWN_STATUS, + label: normalized + .split(/[-_\s]+/u) + .filter(Boolean) + .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) + .join(" "), + }; +} + +export function isTerminalStatus(status: string): boolean { + return statusDisplay(status).terminal; +} diff --git a/web/submitqueue/src/test-setup.ts b/web/submitqueue/src/test-setup.ts new file mode 100644 index 000000000..f149f27ae --- /dev/null +++ b/web/submitqueue/src/test-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/web/submitqueue/src/testing.ts b/web/submitqueue/src/testing.ts new file mode 100644 index 000000000..c9e5f5bd8 --- /dev/null +++ b/web/submitqueue/src/testing.ts @@ -0,0 +1,75 @@ +import type { + GatewayHistoryEvent, + GatewayReader, + GatewayRequestSummary, +} from "./server.js"; + +const DEFAULT_TIMESTAMP = 1_700_000_000_000n; + +export function gatewayRequestFixture( + overrides: Partial = {}, +): GatewayRequestSummary { + return { + sqid: "demo-queue/1", + queue: "demo-queue", + changeUris: ["github://github.com/uber/submitqueue/pull/1/0123456789012345678901234567890123456789"], + receivedAtMs: DEFAULT_TIMESTAMP, + status: "speculating", + lastError: "", + metadata: {}, + ...overrides, + }; +} + +export function gatewayHistoryFixture( + overrides: Partial = {}, +): GatewayHistoryEvent { + return { + timestampMs: DEFAULT_TIMESTAMP, + status: "started", + lastError: "", + metadata: {}, + type: "status", + event: "", + ...overrides, + }; +} + +export interface FakeGatewayReaderOptions { + requests?: readonly GatewayRequestSummary[]; + nextPageToken?: string; + summary?: GatewayRequestSummary; + history?: readonly GatewayHistoryEvent[]; + listError?: unknown; + summaryError?: unknown; + historyError?: unknown; +} + +export function createFakeGatewayReader(options: FakeGatewayReaderOptions = {}): GatewayReader { + const summary = options.summary ?? options.requests?.[0] ?? gatewayRequestFixture(); + return { + async list() { + if (options.listError !== undefined) { + throw options.listError; + } + return { + requests: options.requests ?? [summary], + nextPageToken: options.nextPageToken ?? "", + }; + }, + async getRequestSummaryByID() { + if (options.summaryError !== undefined) { + throw options.summaryError; + } + return { request: summary }; + }, + async getRequestHistoryByID() { + if (options.historyError !== undefined) { + throw options.historyError; + } + return { events: options.history ?? [gatewayHistoryFixture()] }; + }, + }; +} + +export type { GatewayHistoryEvent, GatewayReader, GatewayRequestSummary } from "./server.js"; diff --git a/web/submitqueue/tsconfig.build.json b/web/submitqueue/tsconfig.build.json new file mode 100644 index 000000000..ba8a166de --- /dev/null +++ b/web/submitqueue/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "types": [] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test-setup.ts"] +} diff --git a/web/submitqueue/tsconfig.json b/web/submitqueue/tsconfig.json new file mode 100644 index 000000000..9272498a9 --- /dev/null +++ b/web/submitqueue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "vitest.config.ts"] +} diff --git a/web/submitqueue/vitest.config.ts b/web/submitqueue/vitest.config.ts new file mode 100644 index 000000000..c0ea710e0 --- /dev/null +++ b/web/submitqueue/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./src/test-setup.ts"], + }, +}); diff --git a/web/tool/check-packages.mjs b/web/tool/check-packages.mjs new file mode 100644 index 000000000..75bd197d8 --- /dev/null +++ b/web/tool/check-packages.mjs @@ -0,0 +1,121 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const temporaryRoot = mkdtempSync(join(tmpdir(), "submitqueue-web-packages-")); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd, + encoding: "utf8", + env: options.env ?? process.env, + stdio: options.capture ? "pipe" : "inherit", + }); + if (result.status !== 0) { + if (options.capture) { + process.stderr.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + } + throw new Error(`${command} ${args.join(" ")} failed`); + } +} + +try { + run("corepack", [ + "pnpm@10.17.1", + "--filter", + "@submitqueue/api", + "pack", + "--pack-destination", + temporaryRoot, + ]); + run("corepack", [ + "pnpm@10.17.1", + "--filter", + "@submitqueue/web-submitqueue", + "pack", + "--pack-destination", + temporaryRoot, + ]); + + const archives = readdirSync(temporaryRoot).filter((name) => name.endsWith(".tgz")); + const apiArchive = archives.find((name) => name.includes("submitqueue-api-")); + const libraryArchive = archives.find((name) => name.includes("web-submitqueue-")); + if (!apiArchive || !libraryArchive) { + throw new Error(`expected API and library archives, found: ${archives.join(", ")}`); + } + + writeFileSync( + join(temporaryRoot, "package.json"), + `${JSON.stringify( + { + private: true, + type: "module", + dependencies: { + "@bufbuild/protobuf": "2.15.0", + "@connectrpc/connect": "2.1.1", + "@submitqueue/api": `file:${join(temporaryRoot, apiArchive)}`, + "@submitqueue/web-submitqueue": `file:${join(temporaryRoot, libraryArchive)}`, + "@types/react": "19.1.16", + "@types/react-dom": "19.1.9", + next: "16.3.6", + react: "19.2.0", + "react-dom": "19.2.0", + typescript: "5.9.3", + }, + pnpm: { + overrides: { + "@submitqueue/api": `file:${join(temporaryRoot, apiArchive)}`, + }, + }, + }, + null, + 2, + )}\n`, + ); + writeFileSync( + join(temporaryRoot, "consumer.ts"), + [ + 'import { RequestList } from "@submitqueue/web-submitqueue";', + 'import { loadRequestList } from "@submitqueue/web-submitqueue/server";', + 'import { createFakeGatewayReader } from "@submitqueue/web-submitqueue/testing";', + "void RequestList;", + "void loadRequestList;", + "void createFakeGatewayReader;", + "", + ].join("\n"), + ); + writeFileSync( + join(temporaryRoot, "tsconfig.json"), + `${JSON.stringify( + { + compilerOptions: { + module: "ESNext", + moduleResolution: "Bundler", + noEmit: true, + skipLibCheck: true, + strict: true, + target: "ES2023", + }, + include: ["consumer.ts"], + }, + null, + 2, + )}\n`, + ); + + run( + "corepack", + ["pnpm@10.17.1", "install", "--ignore-scripts", "--no-frozen-lockfile"], + { + cwd: temporaryRoot, + capture: true, + env: { ...process.env, npm_config_registry: "https://registry.npmjs.org" }, + }, + ); + run("corepack", ["pnpm@10.17.1", "exec", "tsc"], { cwd: temporaryRoot }); + console.log("package consumer imports passed"); +} finally { + rmSync(temporaryRoot, { recursive: true, force: true }); +} diff --git a/web/tool/generate-api.mjs b/web/tool/generate-api.mjs new file mode 100644 index 000000000..63051f11f --- /dev/null +++ b/web/tool/generate-api.mjs @@ -0,0 +1,82 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(webRoot, ".."); +const generatedRoot = join(webRoot, "api", "src", "gen"); +const check = process.argv.includes("--check"); +const temporaryRoot = mkdtempSync(join(tmpdir(), "submitqueue-web-api-")); +const outputRoot = check ? join(temporaryRoot, "api", "src", "gen") : generatedRoot; +const buf = join(webRoot, "node_modules", ".bin", process.platform === "win32" ? "buf.cmd" : "buf"); +const plugin = join(webRoot, "node_modules", ".bin", process.platform === "win32" ? "protoc-gen-es.cmd" : "protoc-gen-es"); +const protoFiles = [ + "api/base/change/proto/change.proto", + "api/base/mergestrategy/proto/mergestrategy.proto", + "api/submitqueue/gateway/proto/gateway.proto", +]; + +function listFiles(root, current = root) { + if (!existsSync(current)) { + return []; + } + return readdirSync(current, { withFileTypes: true }).flatMap((entry) => { + const path = join(current, entry.name); + return entry.isDirectory() ? listFiles(root, path) : [relative(root, path)]; + }).sort(); +} + +function generate() { + if (!existsSync(buf) || !existsSync(plugin)) { + throw new Error("Buf and protoc-gen-es are not installed; run pnpm install from web/"); + } + const template = check ? join(temporaryRoot, "buf.gen.yaml") : join(webRoot, "buf.gen.yaml"); + if (check) { + writeFileSync(template, `version: v2\nclean: true\nplugins:\n - local: protoc-gen-es\n out: ${JSON.stringify(outputRoot)}\n opt:\n - target=ts\n - import_extension=js\n`); + } + const path = [join(webRoot, "node_modules", ".bin"), process.env.PATH].filter(Boolean).join(process.platform === "win32" ? ";" : ":"); + const result = spawnSync(buf, [ + "generate", + repositoryRoot, + `--template=${template}`, + ...protoFiles.flatMap((protoFile) => ["--path", join("..", protoFile)]), + ], { + cwd: webRoot, + encoding: "utf8", + env: { ...process.env, PATH: path }, + }); + if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); + } +} + +function verifyDrift() { + const expectedFiles = listFiles(outputRoot); + const actualFiles = listFiles(generatedRoot); + const mismatches = []; + if (expectedFiles.join("\n") !== actualFiles.join("\n")) { + mismatches.push("generated file list differs"); + } + for (const path of expectedFiles) { + const actualPath = join(generatedRoot, path); + if (!existsSync(actualPath) || readFileSync(join(outputRoot, path), "utf8") !== readFileSync(actualPath, "utf8")) { + mismatches.push(path); + } + } + if (mismatches.length > 0) { + process.stderr.write(`Generated API is stale (${mismatches.join(", ")}). Run pnpm api:generate.\n`); + process.exitCode = 1; + } +} + +try { + generate(); + if (check) { + verifyDrift(); + } +} finally { + rmSync(temporaryRoot, { recursive: true, force: true }); +} diff --git a/web/tool/run-e2e.sh b/web/tool/run-e2e.sh new file mode 100755 index 000000000..acf763e8c --- /dev/null +++ b/web/tool/run-e2e.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +project="e2e-submitqueue-web" +token="submitqueue-e2e-local-token-000000000000" +gate_dir="$(mktemp -d -t submitqueue-web-e2e.XXXXXX)" +compose=( + docker-compose + -f "$repo_root/service/submitqueue/docker-compose.yml" + -f "$repo_root/service/submitqueue/docker-compose.fake.yml" + -f "$repo_root/service/submitqueue/docker-compose.web.yml" + -p "$project" +) + +cleanup() { + local status=$? + "${compose[@]}" down -v --remove-orphans || true + rm -rf "$gate_dir" + return "$status" +} +trap cleanup EXIT + +export REPO_ROOT="$repo_root" +export SQ_PROVIDER_CONFIG_DIR="$repo_root/service/submitqueue/demo/provider/fake" +export SQ_CONSUMER_GATE_DIR="$gate_dir" +export SUBMITQUEUE_WEB_TOKEN="$token" +export SQ_MYSQL_INITDB_SKIP_TZINFO=1 + +if docker info --format '{{json .SecurityOptions}}' | grep -q 'name=rootless'; then + export SQ_CONTAINER_USER=0:0 +else + export SQ_CONTAINER_USER="$(id -u):$(id -g)" +fi + +"${compose[@]}" up -d --build --wait + +for schema in "$repo_root"/submitqueue/extension/storage/mysql/schema/*.sql; do + docker exec -i "$project-mysql-app-1" mysql -uroot -proot submitqueue < "$schema" +done +for schema in "$repo_root"/platform/extension/counter/mysql/schema/*.sql; do + docker exec -i "$project-mysql-app-1" mysql -uroot -proot submitqueue < "$schema" +done +for schema in "$repo_root"/platform/extension/messagequeue/mysql/schema/*.sql; do + docker exec -i "$project-mysql-queue-1" mysql -uroot -proot submitqueue < "$schema" +done + +gateway_port="$(docker port "$project-gateway-service-1" 8080/tcp | head -1 | sed 's/.*://')" +web_port="$(docker port "$project-web-service-1" 3000/tcp | head -1 | sed 's/.*://')" +export SUBMITQUEUE_E2E_GATEWAY_URL="http://127.0.0.1:$gateway_port" +export SUBMITQUEUE_E2E_WEB_URL="http://127.0.0.1:$web_port" + +corepack pnpm@10.17.1 --dir "$repo_root/web" e2e diff --git a/web/tsconfig.base.json b/web/tsconfig.base.json new file mode 100644 index 000000000..1fa82e87f --- /dev/null +++ b/web/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowJs": false, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2023" + } +} diff --git a/web/tsconfig.e2e.json b/web/tsconfig.e2e.json new file mode 100644 index 000000000..76c390d3e --- /dev/null +++ b/web/tsconfig.e2e.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["e2e/**/*.ts", "playwright.config.ts"] +} diff --git a/web/vitest.config.ts b/web/vitest.config.ts new file mode 100644 index 000000000..9b6cee87b --- /dev/null +++ b/web/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + clearMocks: true, + coverage: { + provider: "v8", + }, + }, +});