From 5b67f948a1c8edef3f4b5c041e92a87fb8fd61f9 Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Mon, 28 Sep 2026 11:31:02 -0700 Subject: [PATCH] feat(web): add read-only SubmitQueue request UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ### Why? SubmitQueue had gateway APIs for request summaries and history but no simple browser experience for local demos or status inspection. ### What? Add an isolated pnpm workspace with generated TypeScript protobuf bindings, a packaged read-only React library, and a Basic-authenticated Next.js reference host for the hard-coded demo queue. The host lists the trailing 24 hours of requests, safely routes slash-containing SQIDs, shows lifecycle and build history, and refreshes without browser-to-gateway access. Add Compose and Make integration, package-consumer and component coverage, a real-stack Playwright and axe scenario, local documentation, and required web CI jobs. ## Test Plan - ✅ `make lint` - ✅ `make check-tidy` - ✅ `make check-gazelle` - ✅ `make build` - ✅ `make test` - ✅ `PATH=/Users/preetam/.nvm/versions/node/v24.19.0/bin:$PATH make web-check` (69 tests; generation, packaging, lint, typecheck, and Next production build passed) - ✅ Compose configuration, E2E shell syntax, and Playwright test discovery validated locally - ⚠️ `make web-e2e-test` could not run locally because the Docker daemon was unavailable; the required CI job runs the full real-stack Playwright and axe test --- .bazelignore | 1 + .dockerignore | 10 + .github/workflows/ci.yml | 41 + .gitignore | 8 + Makefile | 40 +- doc/howto/QUICKSTART.md | 5 + service/submitqueue/docker-compose.web.yml | 34 + web/.nvmrc | 1 + web/api/package.json | 32 + web/api/src/api.test.ts | 23 + .../gen/api/base/change/proto/change_pb.ts | 74 + .../mergestrategy/proto/mergestrategy_pb.ts | 95 + .../submitqueue/gateway/proto/gateway_pb.ts | 822 +++ web/api/tsconfig.build.json | 11 + web/api/tsconfig.json | 7 + web/buf.gen.yaml | 8 + web/e2e/submitqueue.spec.ts | 103 + web/go.mod | 3 + web/package.json | 33 + web/playwright.config.ts | 26 + web/pnpm-lock.yaml | 5968 +++++++++++++++++ web/pnpm-workspace.yaml | 8 + web/service/submitqueue/.gitignore | 3 + web/service/submitqueue/Dockerfile | 38 + web/service/submitqueue/eslint.config.mjs | 7 + web/service/submitqueue/next-env.d.ts | 7 + web/service/submitqueue/next.config.ts | 13 + web/service/submitqueue/package.json | 34 + .../src/app/(protected)/layout.tsx | 31 + .../submitqueue/src/app/(protected)/page.tsx | 18 + .../app/(protected)/requests/[sqid]/page.tsx | 87 + .../src/app/(protected)/requests/page.tsx | 79 + web/service/submitqueue/src/app/globals.css | 505 ++ .../submitqueue/src/app/healthz/route.test.ts | 13 + .../submitqueue/src/app/healthz/route.ts | 8 + web/service/submitqueue/src/app/layout.tsx | 20 + web/service/submitqueue/src/app/not-found.tsx | 16 + .../submitqueue/src/app/unauthorized.tsx | 11 + web/service/submitqueue/src/proxy.test.ts | 46 + web/service/submitqueue/src/proxy.ts | 21 + .../submitqueue/src/server/auth.test.ts | 54 + web/service/submitqueue/src/server/auth.ts | 86 + .../submitqueue/src/server/config.test.ts | 40 + web/service/submitqueue/src/server/config.ts | 45 + web/service/submitqueue/src/server/gateway.ts | 28 + .../submitqueue/src/server/request-auth.ts | 18 + .../submitqueue/src/server/window.test.ts | 41 + web/service/submitqueue/src/server/window.ts | 54 + .../submitqueue/src/test/server-only.ts | 1 + web/service/submitqueue/tsconfig.json | 36 + web/service/submitqueue/vitest.config.ts | 13 + web/submitqueue/.gitignore | 2 + web/submitqueue/package.json | 53 + web/submitqueue/src/auto-refresh.test.tsx | 87 + web/submitqueue/src/auto-refresh.tsx | 100 + web/submitqueue/src/components.test.tsx | 100 + web/submitqueue/src/components.tsx | 180 + web/submitqueue/src/index.ts | 32 + web/submitqueue/src/models.ts | 55 + web/submitqueue/src/paths.test.ts | 27 + web/submitqueue/src/paths.ts | 79 + web/submitqueue/src/server.test.ts | 129 + web/submitqueue/src/server.ts | 293 + web/submitqueue/src/status.test.ts | 24 + web/submitqueue/src/status.ts | 49 + web/submitqueue/src/test-setup.ts | 1 + web/submitqueue/src/testing.ts | 75 + web/submitqueue/tsconfig.build.json | 12 + web/submitqueue/tsconfig.json | 17 + web/submitqueue/vitest.config.ts | 9 + web/tool/check-packages.mjs | 121 + web/tool/generate-api.mjs | 82 + web/tool/run-e2e.sh | 54 + web/tsconfig.base.json | 18 + web/tsconfig.e2e.json | 7 + web/vitest.config.ts | 10 + 76 files changed, 10334 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100644 service/submitqueue/docker-compose.web.yml create mode 100644 web/.nvmrc create mode 100644 web/api/package.json create mode 100644 web/api/src/api.test.ts create mode 100644 web/api/src/gen/api/base/change/proto/change_pb.ts create mode 100644 web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts create mode 100644 web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts create mode 100644 web/api/tsconfig.build.json create mode 100644 web/api/tsconfig.json create mode 100644 web/buf.gen.yaml create mode 100644 web/e2e/submitqueue.spec.ts create mode 100644 web/go.mod create mode 100644 web/package.json create mode 100644 web/playwright.config.ts create mode 100644 web/pnpm-lock.yaml create mode 100644 web/pnpm-workspace.yaml create mode 100644 web/service/submitqueue/.gitignore create mode 100644 web/service/submitqueue/Dockerfile create mode 100644 web/service/submitqueue/eslint.config.mjs create mode 100644 web/service/submitqueue/next-env.d.ts create mode 100644 web/service/submitqueue/next.config.ts create mode 100644 web/service/submitqueue/package.json create mode 100644 web/service/submitqueue/src/app/(protected)/layout.tsx create mode 100644 web/service/submitqueue/src/app/(protected)/page.tsx create mode 100644 web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx create mode 100644 web/service/submitqueue/src/app/(protected)/requests/page.tsx create mode 100644 web/service/submitqueue/src/app/globals.css create mode 100644 web/service/submitqueue/src/app/healthz/route.test.ts create mode 100644 web/service/submitqueue/src/app/healthz/route.ts create mode 100644 web/service/submitqueue/src/app/layout.tsx create mode 100644 web/service/submitqueue/src/app/not-found.tsx create mode 100644 web/service/submitqueue/src/app/unauthorized.tsx create mode 100644 web/service/submitqueue/src/proxy.test.ts create mode 100644 web/service/submitqueue/src/proxy.ts create mode 100644 web/service/submitqueue/src/server/auth.test.ts create mode 100644 web/service/submitqueue/src/server/auth.ts create mode 100644 web/service/submitqueue/src/server/config.test.ts create mode 100644 web/service/submitqueue/src/server/config.ts create mode 100644 web/service/submitqueue/src/server/gateway.ts create mode 100644 web/service/submitqueue/src/server/request-auth.ts create mode 100644 web/service/submitqueue/src/server/window.test.ts create mode 100644 web/service/submitqueue/src/server/window.ts create mode 100644 web/service/submitqueue/src/test/server-only.ts create mode 100644 web/service/submitqueue/tsconfig.json create mode 100644 web/service/submitqueue/vitest.config.ts create mode 100644 web/submitqueue/.gitignore create mode 100644 web/submitqueue/package.json create mode 100644 web/submitqueue/src/auto-refresh.test.tsx create mode 100644 web/submitqueue/src/auto-refresh.tsx create mode 100644 web/submitqueue/src/components.test.tsx create mode 100644 web/submitqueue/src/components.tsx create mode 100644 web/submitqueue/src/index.ts create mode 100644 web/submitqueue/src/models.ts create mode 100644 web/submitqueue/src/paths.test.ts create mode 100644 web/submitqueue/src/paths.ts create mode 100644 web/submitqueue/src/server.test.ts create mode 100644 web/submitqueue/src/server.ts create mode 100644 web/submitqueue/src/status.test.ts create mode 100644 web/submitqueue/src/status.ts create mode 100644 web/submitqueue/src/test-setup.ts create mode 100644 web/submitqueue/src/testing.ts create mode 100644 web/submitqueue/tsconfig.build.json create mode 100644 web/submitqueue/tsconfig.json create mode 100644 web/submitqueue/vitest.config.ts create mode 100644 web/tool/check-packages.mjs create mode 100644 web/tool/generate-api.mjs create mode 100755 web/tool/run-e2e.sh create mode 100644 web/tsconfig.base.json create mode 100644 web/tsconfig.e2e.json create mode 100644 web/vitest.config.ts 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/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", + }, + }, +});