diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..75913c6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git/ +.github/ +.idea/ +.vscode/ +*.db +*.db-journal +*.db-wal +coverage.out +dist/ +web/node_modules/ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e11a389 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.go] +indent_style = tab + +[*.{yaml,yml,md,json}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index 70a54d7..3cf92b1 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -38,7 +38,9 @@ jobs: - name: Set up Go uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 with: - go-version: '1.24' + go-version: '1.26' + - name: Set up pnpm + uses: pnpm/action-setup@v4 - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 name: Set up Node with: diff --git a/.github/workflows/vet.yaml b/.github/workflows/vet.yaml index aca445e..1552bda 100644 --- a/.github/workflows/vet.yaml +++ b/.github/workflows/vet.yaml @@ -13,48 +13,31 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go_version: [ 1.24.x ] + go_version: [ 1.26.x ] steps: - name: Set up Go ${{ matrix.go_version }} - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version: ${{ matrix.go_version }} - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: golangci-lint - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 + # v8+ of the action is required for golangci-lint v2 (older action + # versions pass --out-format, which v2 removed) + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.64 - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. + version: v2.12.2 args: --timeout=5m - - # Optional: show only new issues if it's a pull request. The default value is `false`. - only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true golang-test: runs-on: ubuntu-latest strategy: matrix: - go_version: [ 1.24.x ] + go_version: [ 1.26.x ] steps: - name: Set up Go ${{ matrix.go_version }} - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version: ${{ matrix.go_version }} - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: run tests run: go test -json ./... > test.json - name: Annotate tests @@ -71,17 +54,25 @@ jobs: matrix: node-version: [22.x] steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Set up pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + with: + version: 10.30.1 + run_install: false - name: Set up NodeJS ${{ matrix.node-version }} - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} - cache: "npm" - cache-dependency-path: web/package-lock.json + cache: "pnpm" + cache-dependency-path: web/pnpm-lock.yaml - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile + - name: Install Playwright browsers + # the Storybook test suite runs in a real Chromium via Vitest browser mode + run: pnpm exec playwright install --with-deps chromium - name: Test - run: npm test + run: pnpm test web-lint: runs-on: ubuntu-latest defaults: @@ -91,23 +82,28 @@ jobs: matrix: node-version: [22.x] steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Set up pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 + with: + version: 10.30.1 + run_install: false - name: Set up NodeJS ${{ matrix.node-version }} - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node-version }} - cache: "npm" - cache-dependency-path: web/package-lock.json + cache: "pnpm" + cache-dependency-path: web/pnpm-lock.yaml - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Lint - run: npm run lint + run: pnpm lint - name: Save Report - run: npm run lint:report + run: pnpm lint:report continue-on-error: true - name: Annotate Results uses: ataylorme/eslint-annotate-action@5f4dc2e3af8d3c21b727edb597e5503510b1dc9c # 2.2.0 - if: "!github.event.pull_request.head.repo.fork" + if: "!github.event.pull_request.head.repo.fork" with: repo-token: "${{ secrets.GITHUB_TOKEN }}" report-json: "web/eslint_report.json" diff --git a/.gitignore b/.gitignore index f3d39a6..f36d817 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ bin +/forky +.playwright-mcp/ +coverage.out config.yaml config2.yaml __debug_bin* dist/ +web/dist/ checkpointz store.db index.db diff --git a/.golangci.yml b/.golangci.yml index 98caad7..83ee435 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,65 +1,132 @@ -linters-settings: - errcheck: - check-type-assertions: true - goconst: - min-len: 2 - min-occurrences: 3 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - govet: - shadow: true - nolintlint: - require-explanation: true - require-specific: true +version: "2" + +run: + timeout: 5m linters: - disable-all: true + default: none enable: - - asasalint - bidichk - bodyclose - - containedctx - - decorder - - dogsled - - durationcheck + - copyloopvar + - cyclop + - dupword - errcheck - errname - - copyloopvar + - errorlint + - exhaustive + - exptostd + - fatcontext + - funlen + - gocognit - gocritic - - gocyclo - - gofmt - - goheader - - goimports - gosec - - gosimple - govet - ineffassign + - intrange - misspell - - nakedret - - nilerr + - modernize + - nestif - nilerr - - nilnil - - nlreturn + - noctx - nolintlint - - nosprintfhostport - - prealloc + - perfsprint - predeclared - - promlinter - - reassign + - revive + - sloglint - staticcheck - - stylecheck - - thelper + - tagliatelle + - testifylint - tparallel - - typecheck - unconvert + - usetesting + - unparam - unused + - usestdlibvars + - wastedassign - whitespace - - wsl -run: - issues-exit-code: 1 + settings: + cyclop: + max-complexity: 30 + + funlen: + lines: 100 + statements: 50 + + gocognit: + # Aligned with cyclop's max-complexity so the two complexity metrics agree. + min-complexity: 30 + + errcheck: + check-type-assertions: true + + exhaustive: + # A `default` case counts as handling the remaining enum members. Avoids + # enumerating large generated enums (e.g. xatu.Event_Name) exhaustively. + default-signifies-exhaustive: true + + tagliatelle: + case: + rules: + yaml: snake + json: snake + + revive: + rules: + - name: unexported-return + disabled: true + + govet: + enable-all: true + disable: + - fieldalignment + settings: + shadow: + # Non-strict: don't flag the idiomatic `if x, err := f(); err != nil` + # reuse of an outer err in nested scopes. + strict: false + + nolintlint: + require-explanation: true + require-specific: true + + sloglint: + no-global: all + context: scope + + staticcheck: + checks: + - all + + exclusions: + paths: + # ogen-generated server/client code. + - api/rest + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + # Test files are allowed to be long/complex (table-driven tests) and to + # have unused helper parameters. + - path: _test\.go + linters: + - funlen + - gocognit + - cyclop + - unparam + +formatters: + enable: + - gofumpt + - goimports + settings: + goimports: + local-prefixes: + - github.com/ethpandaops/forky + exclusions: + paths: + # ogen-generated server/client code. + - api/rest diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4af9927..2cb79ca 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,6 +9,9 @@ before: - make build-web builds: - binary: forky-{{.Version}} + main: ./cmd/forky + tags: + - webui goos: - linux - windows @@ -24,7 +27,7 @@ builds: ignore: - goarch: 386 ldflags: - - -s -w -X github.com/ethpandaops/forky/pkg/version.Release={{.Tag}} -X github.com/ethpandaops/forky/pkg/version.GitCommit={{.ShortCommit}} + - -s -w -X github.com/ethpandaops/forky/internal/version.Release={{.Tag}} -X github.com/ethpandaops/forky/internal/version.GitCommit={{.ShortCommit}} mod_timestamp: "{{ .CommitTimestamp }}" checksum: diff --git a/.redocly.yaml b/.redocly.yaml new file mode 100644 index 0000000..dc9aa8a --- /dev/null +++ b/.redocly.yaml @@ -0,0 +1,13 @@ +extends: + - recommended + +rules: + info-license: error + no-server-example.com: error + tag-description: error + operation-4xx-response: error + no-unused-components: error + operation-2xx-response: off + # Forky's API is unauthenticated (read-only fork-choice data). Re-enable once + # an auth scheme is added. + security-defined: off diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..cce6461 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +golang 1.26.2 +golangci-lint 2.12.2 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..715e0de --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# forky + +Forky captures, stores, indexes and serves Ethereum Beacon Chain fork-choice +data, providing a live and historical view of the network. It runs as a single +service with an embedded operator web UI. + +## Architecture + +- `cmd/forky/main.go` — cobra CLI entrypoint; builds the logger, loads config, + constructs the server and runs it until a signal is received. +- `forkchoice.go` / `http.go` / `config.go` — application root (package `forky` + at the repo root). `NewServer` wires dependencies; `Start` runs the service. + `NewConfigFromYAMLFile` loads `Config` from YAML. +- `internal/` — application packages, each a cohesive capability: + - `store/` — frame storage backends (memory, filesystem, S3). + - `source/` — fork-choice data sources (beacon node, Xatu). + - `service/` — fork-choice orchestration, backfill, pagination, purging. + - `db/` — frame metadata indexer (SQLite, Postgres). + - `api/` — HTTP handlers for the JSON API under `/api/v1`. + - `ethereum/`, `types/`, `human/` — domain support. + - `version/` — build metadata (set via `-ldflags`). + - `yaml/` — raw config helpers. +- `web/` — operator UI (React + Vite). The built assets are embedded into the + binary and served by the API server. See `web/CLAUDE.md`. + +New subsystems get their own package under `internal/` and are wired in from the +root `forky` package. + +## Conventions + +- Go: `github.com/sirupsen/logrus` for logging (existing project — do not mix + logging libraries), `fmt.Errorf("...: %w", err)` for error wrapping. Linting + is governed by `.golangci.yml`. +- Web: see `web/CLAUDE.md`. Colors must use the semantic tokens in + `web/src/index.css` (enforced by custom ESLint rules). +- The OpenAPI spec in `api/openapi.yaml` is the source of truth for the typed + web API client (`pnpm --dir web generate:api`). + +## Commands + +```sh +make build # compile the binary +make build-web # build the embedded frontend assets +make test # go test -race +make lint # golangci-lint +make audit # lint + test + vuln + modernize + mod verify +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..962cc9c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# forky + +Forky captures, stores, indexes and serves Ethereum Beacon Chain fork-choice +data, providing a live and historical view of the network. It runs as a single +service with an embedded operator web UI. + +## Architecture + +- `cmd/forky/main.go` — cobra CLI entrypoint; builds the logger, loads config, + constructs the server and runs it until a signal is received. +- `forkchoice.go` / `http.go` / `config.go` — application root (package `forky` + at the repo root). `NewServer` wires dependencies; `Start` runs the service + on a `http.ServeMux` (`/api/` → generated server, `/openapi.yaml` → embedded + spec, `/` → SPA fallback). `NewConfigFromYAMLFile` loads `Config` from YAML. +- `api/` — the OpenAPI spec (`openapi.yaml`, the source of truth for the HTTP + API), the embedded spec (`spec.go`) and the ogen-generated server/client in + `api/rest` (generated — do not edit; see `generate.go`). +- `internal/` — application packages, each a cohesive capability: + - `store/` — frame storage backends (memory, filesystem, S3). + - `source/` — fork-choice data sources (beacon node, Xatu). + - `service/` — fork-choice orchestration, backfill, pagination, purging. + - `db/` — frame metadata indexer (SQLite, Postgres). + - `api/` — implements the generated `rest.Handler` interface; maps domain + types onto the spec schemas. + - `ethereum/`, `types/`, `human/` — domain support. + - `version/` — build metadata (set via `-ldflags`). + - `yaml/` — raw config helpers. +- `web/` — operator UI (React + Vite). The built assets are embedded into the + binary and served by the API server. See `web/CLAUDE.md`. + +New subsystems get their own package under `internal/` and are wired in from the +root `forky` package. + +## Conventions + +- Go: `github.com/sirupsen/logrus` for logging (existing project — do not mix + logging libraries), `fmt.Errorf("...: %w", err)` for error wrapping. Linting + is governed by `.golangci.yml`. +- Web: see `web/CLAUDE.md`. Colors must use the semantic tokens in + `web/src/index.css` (enforced by custom ESLint rules). +- The OpenAPI spec in `api/openapi.yaml` is the source of truth for the HTTP + API on both sides: the Go server (`api/rest`, generated by ogen) and the + typed web API client (`web/src/api`, generated by @hey-api/openapi-ts). + After editing the spec, run `make generate` to regenerate both. + +## Commands + +```sh +make build # compile the binary +make build-web # build the embedded frontend assets +make generate # regenerate the ogen server + web client from api/openapi.yaml +make test # go test -race +make lint # golangci-lint +make audit # lint + test + vuln + modernize + mod verify +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..06e5f96 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing to Forky + +## Prerequisites + +- [Go](https://go.dev/dl/) (see `go.mod` for the minimum version) +- [Node.js](https://nodejs.org/) + [pnpm](https://pnpm.io/) (for the web UI) +- [golangci-lint](https://golangci-lint.run/welcome/install/) +- [goreleaser](https://goreleaser.com/install/) (optional, for release checks) + +## Development Workflow + +```sh +# Build the binary with the embedded web UI +make build + +# Run tests +make test + +# Run the linter +make lint + +# Run all checks (lint + test + vuln + modernize + mod verify) +make audit +``` + +The backend can be run directly against a config file: + +```sh +make run # build + run with ./config.yaml +# or +go run ./cmd/forky --config your_config.yaml +``` + +The web UI has its own workflow — see [`web/CLAUDE.md`](web/CLAUDE.md). + +## Pull Request Guidelines + +1. Create a feature branch from `master`. +2. Keep changes focused — one concern per PR. +3. Add tests for new functionality. +4. Ensure `make audit` passes before submitting. +5. Use [Conventional Commits](https://www.conventionalcommits.org/) — the web + package enforces them via commitlint. + +## Code Style + +- Follow the conventions enforced by `golangci-lint` and `.golangci.yml`. +- Use `github.com/sirupsen/logrus` for logging (consistent with the existing + codebase — do not mix logging libraries). +- Wrap errors with context using `fmt.Errorf` and `%w`. +- The OpenAPI spec in `api/openapi.yaml` is the source of truth for the typed + web API client (`pnpm --dir web generate:api`). diff --git a/Dockerfile b/Dockerfile index 9a2c3fa..202c160 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM golang:1.19 AS builder +FROM golang:1.26 AS builder WORKDIR /src COPY go.sum go.mod ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -o /bin/app . +RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/forky FROM ubuntu:latest RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..af26637 --- /dev/null +++ b/Makefile @@ -0,0 +1,94 @@ +.DEFAULT_GOAL := help +.PHONY: build build-web setup-frontend generate lint lint-openapi fmt test clean tidy vuln modernize-check audit check-release run test/cover help + +BUILD_DIR := ./cmd/forky +BINARY := forky +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo none) +LDFLAGS := -s -w \ + -X github.com/ethpandaops/forky/internal/version.Release=$(VERSION) \ + -X github.com/ethpandaops/forky/internal/version.GitCommit=$(COMMIT) + +## setup-frontend: ensure web/dist/index.html exists for the embed +setup-frontend: + @mkdir -p web/dist + @test -f web/dist/index.html || echo 'forky

forky

' > web/dist/index.html + +## build-web: build the embedded frontend assets (output to web/dist) +build-web: setup-frontend + @if command -v pnpm >/dev/null 2>&1; then \ + echo "building embedded web assets with pnpm"; \ + cd web && pnpm install && pnpm build; \ + else \ + echo "pnpm not found, skipping web build and using existing web/dist assets"; \ + fi + +## build: compile the binary with the embedded frontend +build: setup-frontend + go build -tags=webui -trimpath -ldflags="$(LDFLAGS)" -o $(BINARY) $(BUILD_DIR) + +## generate: regenerate the ogen server (api/rest) and web API client from api/openapi.yaml +generate: + go generate ./... + @if command -v pnpm >/dev/null 2>&1; then \ + cd web && pnpm generate:api; \ + else \ + echo "pnpm not found, skipping web API client generation"; \ + fi + +## lint: run golangci-lint (only new issues vs origin/master when available) +lint: + @if git rev-parse --verify --quiet origin/master >/dev/null; then \ + golangci-lint run --new-from-rev="origin/master" ./...; \ + else \ + golangci-lint run ./...; \ + fi + +## lint-openapi: lint the OpenAPI spec with Redocly +lint-openapi: + npx -y @redocly/cli@latest lint api/openapi.yaml --config .redocly.yaml + +## fmt: format code +fmt: + golangci-lint fmt ./... + +## test: run tests with the race detector +test: + go test -race -shuffle=on -coverprofile=coverage.out -covermode=atomic ./... + +## clean: remove build outputs +clean: + rm -f $(BINARY) coverage.out + +## tidy: tidy go modules +tidy: + go mod tidy + +## vuln: run govulncheck +vuln: + go tool govulncheck ./... + +## modernize-check: preview Go modernizations without changing files +modernize-check: + go fix -n ./... + +## audit: run all checks +audit: lint test vuln modernize-check + go mod tidy -diff + go mod verify + +## run: build and run the binary against ./config.yaml +run: build + ./$(BINARY) --config config.yaml + +## test/cover: open the HTML coverage report +test/cover: test + go tool cover -html=coverage.out + +## check-release: validate the goreleaser config +check-release: + goreleaser check -q + +## help: show this help +help: + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' diff --git a/README.md b/README.md index acf50fd..514ed44 100644 --- a/README.md +++ b/README.md @@ -121,19 +121,19 @@ helm install forky ethereum-helm-charts/forky -f your_values.yaml 1. Clone the repo ```sh - go get github.com/ethpandaops/forky + git clone https://github.com/ethpandaops/forky.git ``` 2. Change directories ```sh cd ./forky ``` -3. Build the binary - ```sh - go build -o forky . +3. Build the binary (embeds the web frontend; requires `pnpm`) + ```sh + make build-web && make build ``` 4. Run the service - ```sh - ./forky + ```sh + ./forky --config your_config.yaml ``` ## Contributing @@ -152,7 +152,7 @@ Contributions are greatly appreciated! Pull requests will be reviewed and merged ### Running locally #### Backend ``` -go run main.go --config your_config.yaml +go run ./cmd/forky --config your_config.yaml ``` #### Frontend @@ -165,7 +165,9 @@ The frontend can be built with the following command; make build-web ``` -Building frontend requires `npm` and `NodeJS` to be installed. +Building the frontend requires `pnpm` and `NodeJS` to be installed. For +frontend development, `pnpm --dir web dev` starts a Vite dev server that +proxies `/api` to a backend (see `web/vite.config.ts`). ## Contact diff --git a/api/generate.go b/api/generate.go new file mode 100644 index 0000000..6f9d6e7 --- /dev/null +++ b/api/generate.go @@ -0,0 +1,7 @@ +// Package api contains the OpenAPI specification and the generated server. +package api + +// The OpenAPI spec at openapi.yaml is the source of truth for the HTTP API. +// The ogen-generated server, client and schema types live in ./rest and are +// regenerated from the spec via `make generate`. +//go:generate go tool ogen --target rest --package rest --clean openapi.yaml diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..6a78ad8 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,558 @@ +openapi: 3.1.0 +info: + title: Forky API + version: 1.0.0 + description: | + Forky captures, stores, indexes and serves Ethereum Beacon Chain + fork-choice data. This API exposes fork-choice frames and their metadata. + + All successful responses wrap their payload in a top-level `data` object. + Error responses return a flat `{ "code", "message" }` object. + + Frame metadata serializes slots and epochs as JSON numbers. The fork-choice + dump inside a frame mirrors the beacon node `/eth/v1/debug/fork_choice` + response, where slots, epochs and weights are serialized as strings. + license: + name: GPL-3.0-or-later + url: https://www.gnu.org/licenses/gpl-3.0.html + +servers: + - url: https://forky.mainnet.ethpandaops.io + description: Mainnet public instance + - url: https://forky.hoodi.ethpandaops.io + description: Hoodi public instance + +tags: + - name: Ethereum + description: Ethereum network timing and spec information. + - name: Metadata + description: Query stored fork-choice frame metadata. + - name: Frames + description: Retrieve individual fork-choice frames. + +paths: + /api/v1/ethereum/now: + get: + operationId: getEthereumNow + summary: Get the current wall-clock slot and epoch + tags: + - Ethereum + responses: + "200": + description: The current wall-clock slot and epoch. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + $ref: "#/components/schemas/EthereumNow" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/ethereum/spec: + get: + operationId: getEthereumSpec + summary: Get the Ethereum network name and spec + tags: + - Ethereum + responses: + "200": + description: The configured network name and spec. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + $ref: "#/components/schemas/EthereumSpecResult" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/frames/{id}: + get: + operationId: getFrame + summary: Get a single fork-choice frame by ID + tags: + - Frames + parameters: + - name: id + in: path + required: true + description: The frame ID. + schema: + type: string + responses: + "200": + description: The requested fork-choice frame. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + type: object + required: [frame] + properties: + frame: + $ref: "#/components/schemas/Frame" + "404": + $ref: "#/components/responses/NotFound" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/metadata: + post: + operationId: listMetadata + summary: List fork-choice frame metadata + tags: + - Metadata + requestBody: + $ref: "#/components/requestBodies/MetadataQuery" + responses: + "200": + description: A page of frame metadata. + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + type: object + required: [frames, pagination] + properties: + frames: + type: array + items: + $ref: "#/components/schemas/FrameMetadata" + pagination: + $ref: "#/components/schemas/PaginationResponse" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/metadata/nodes: + post: + operationId: listMetadataNodes + summary: List distinct nodes across stored frames + tags: + - Metadata + requestBody: + $ref: "#/components/requestBodies/MetadataQuery" + responses: + "200": + description: A page of distinct node names. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + type: object + required: [nodes, pagination] + properties: + nodes: + type: array + items: + type: string + pagination: + $ref: "#/components/schemas/PaginationResponse" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/metadata/slots: + post: + operationId: listMetadataSlots + summary: List distinct slots across stored frames + tags: + - Metadata + requestBody: + $ref: "#/components/requestBodies/MetadataQuery" + responses: + "200": + description: A page of distinct slots. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + type: object + required: [slots, pagination] + properties: + slots: + type: array + items: + $ref: "#/components/schemas/Slot" + pagination: + $ref: "#/components/schemas/PaginationResponse" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/metadata/epochs: + post: + operationId: listMetadataEpochs + summary: List distinct epochs across stored frames + tags: + - Metadata + requestBody: + $ref: "#/components/requestBodies/MetadataQuery" + responses: + "200": + description: A page of distinct epochs. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + type: object + required: [epochs, pagination] + properties: + epochs: + type: array + items: + $ref: "#/components/schemas/Epoch" + pagination: + $ref: "#/components/schemas/PaginationResponse" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + + /api/v1/metadata/labels: + post: + operationId: listMetadataLabels + summary: List distinct labels across stored frames + tags: + - Metadata + requestBody: + $ref: "#/components/requestBodies/MetadataQuery" + responses: + "200": + description: A page of distinct labels. + headers: + Cache-Control: + $ref: "#/components/headers/CacheControl" + content: + application/json: + schema: + type: object + required: [data] + properties: + data: + type: object + required: [labels, pagination] + properties: + labels: + type: array + items: + type: string + pagination: + $ref: "#/components/schemas/PaginationResponse" + "400": + $ref: "#/components/responses/BadRequest" + default: + $ref: "#/components/responses/UnexpectedError" + +components: + headers: + CacheControl: + description: Caching directives for the response. + schema: + type: string + + requestBodies: + MetadataQuery: + description: Filter and pagination for a metadata query. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MetadataQuery" + + responses: + BadRequest: + description: The request was malformed. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + NotFound: + description: The requested resource was not found. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + UnexpectedError: + description: An unexpected error occurred. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + schemas: + Slot: + type: integer + format: int64 + minimum: 0 + description: A beacon-chain slot number. + example: 7654321 + Epoch: + type: integer + format: int64 + minimum: 0 + description: A beacon-chain epoch number. + example: 239197 + SlotString: + type: string + description: | + A beacon-chain slot number serialized as a string, following the + beacon-API convention used by the fork-choice dump. + example: "7654321" + EpochString: + type: string + description: | + A beacon-chain epoch number serialized as a string, following the + beacon-API convention used by the fork-choice dump. + example: "239197" + Root: + type: string + description: A 32-byte hex-encoded root. + example: "0x0000000000000000000000000000000000000000000000000000000000000000" + + Error: + type: object + required: [code, message] + properties: + code: + type: integer + description: The HTTP status code. + message: + type: string + description: A human-readable error message. + + PaginationCursor: + type: object + description: Offset-based pagination cursor. + properties: + offset: + type: integer + description: The number of items to skip. + default: 0 + limit: + type: integer + description: The maximum number of items to return. + default: 1000 + + PaginationResponse: + type: object + required: [total] + properties: + total: + type: integer + format: int64 + description: The total number of matching items. + + FrameFilter: + type: object + description: Filters applied to a frame query. All fields are optional. + properties: + node: + type: string + before: + type: string + format: date-time + after: + type: string + format: date-time + slot: + $ref: "#/components/schemas/Slot" + epoch: + $ref: "#/components/schemas/Epoch" + labels: + type: array + items: + type: string + consensus_client: + type: string + event_source: + type: string + + MetadataQuery: + type: object + properties: + filter: + $ref: "#/components/schemas/FrameFilter" + pagination: + $ref: "#/components/schemas/PaginationCursor" + + FrameMetadata: + type: object + required: + - id + - node + - fetched_at + - wall_clock_slot + - wall_clock_epoch + - labels + - consensus_client + - event_source + properties: + id: + type: string + description: The frame ID. + node: + type: string + description: | + The node that provided the frame. For a beacon node this is the + configured node ID; for Xatu it is the Xatu Sentry ID. + fetched_at: + type: string + format: date-time + description: When the frame was fetched. + wall_clock_slot: + $ref: "#/components/schemas/Slot" + wall_clock_epoch: + $ref: "#/components/schemas/Epoch" + labels: + type: array + items: + type: string + consensus_client: + type: string + event_source: + type: string + + Checkpoint: + type: object + required: [epoch, root] + properties: + epoch: + $ref: "#/components/schemas/EpochString" + root: + $ref: "#/components/schemas/Root" + + ForkChoiceNode: + type: object + required: + - slot + - block_root + - parent_root + - justified_epoch + - finalized_epoch + - weight + - validity + - execution_block_hash + properties: + slot: + $ref: "#/components/schemas/SlotString" + block_root: + $ref: "#/components/schemas/Root" + parent_root: + $ref: "#/components/schemas/Root" + justified_epoch: + $ref: "#/components/schemas/EpochString" + finalized_epoch: + $ref: "#/components/schemas/EpochString" + weight: + type: string + description: The node's fork-choice weight, serialized as a string. + validity: + type: string + description: The node's validity status. + enum: [valid, invalid, optimistic, unknown] + execution_block_hash: + $ref: "#/components/schemas/Root" + extra_data: + type: object + additionalProperties: true + + ForkChoice: + type: object + description: A beacon node's fork-choice dump. + required: [justified_checkpoint, finalized_checkpoint, fork_choice_nodes] + properties: + justified_checkpoint: + $ref: "#/components/schemas/Checkpoint" + finalized_checkpoint: + $ref: "#/components/schemas/Checkpoint" + fork_choice_nodes: + type: array + items: + $ref: "#/components/schemas/ForkChoiceNode" + + Frame: + type: object + required: [data, metadata] + properties: + data: + $ref: "#/components/schemas/ForkChoice" + metadata: + $ref: "#/components/schemas/FrameMetadata" + + EthereumNow: + type: object + required: [slot, epoch] + properties: + slot: + $ref: "#/components/schemas/Slot" + epoch: + $ref: "#/components/schemas/Epoch" + + EthereumSpec: + type: object + required: [seconds_per_slot, slots_per_epoch, genesis_time] + properties: + seconds_per_slot: + type: integer + format: int64 + slots_per_epoch: + type: integer + format: int64 + genesis_time: + type: string + format: date-time + + EthereumSpecResult: + type: object + required: [network_name, spec] + properties: + network_name: + type: string + spec: + $ref: "#/components/schemas/EthereumSpec" diff --git a/api/rest/oas_cfg_gen.go b/api/rest/oas_cfg_gen.go new file mode 100644 index 0000000..9d5a059 --- /dev/null +++ b/api/rest/oas_cfg_gen.go @@ -0,0 +1,318 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "net/http" + "strings" + + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/otelogen" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" +) + +var ( + // Allocate option closure once. + clientSpanKind = trace.WithSpanKind(trace.SpanKindClient) + // Allocate option closure once. + serverSpanKind = trace.WithSpanKind(trace.SpanKindServer) +) + +type ( + optionFunc[C any] func(*C) + otelOptionFunc func(*otelConfig) +) + +type otelConfig struct { + TracerProvider trace.TracerProvider + Tracer trace.Tracer + MeterProvider metric.MeterProvider + Meter metric.Meter + Attributes []attribute.KeyValue +} + +func (cfg *otelConfig) initOTEL() { + if cfg.TracerProvider == nil { + cfg.TracerProvider = otel.GetTracerProvider() + } + if cfg.MeterProvider == nil { + cfg.MeterProvider = otel.GetMeterProvider() + } + cfg.Tracer = cfg.TracerProvider.Tracer(otelogen.Name, + trace.WithInstrumentationVersion(otelogen.SemVersion()), + ) + cfg.Meter = cfg.MeterProvider.Meter(otelogen.Name, + metric.WithInstrumentationVersion(otelogen.SemVersion()), + ) +} + +// ErrorHandler is error handler. +type ErrorHandler = ogenerrors.ErrorHandler + +type serverConfig struct { + otelConfig + NotFound http.HandlerFunc + MethodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string) + ErrorHandler ErrorHandler + Prefix string + Middleware Middleware + MaxMultipartMemory int64 +} + +// ServerOption is server config option. +type ServerOption interface { + applyServer(*serverConfig) +} + +var _ ServerOption = (optionFunc[serverConfig])(nil) + +func (o optionFunc[C]) applyServer(c *C) { + o(c) +} + +var _ ServerOption = (otelOptionFunc)(nil) + +func (o otelOptionFunc) applyServer(c *serverConfig) { + o(&c.otelConfig) +} + +func newServerConfig(opts ...ServerOption) serverConfig { + cfg := serverConfig{ + NotFound: http.NotFound, + MethodNotAllowed: nil, + ErrorHandler: ogenerrors.DefaultErrorHandler, + Middleware: nil, + MaxMultipartMemory: 32 << 20, // 32 MB + } + for _, opt := range opts { + opt.applyServer(&cfg) + } + cfg.initOTEL() + return cfg +} + +type baseServer struct { + cfg serverConfig + requests metric.Int64Counter + errors metric.Int64Counter + duration metric.Float64Histogram +} + +func (s baseServer) notFound(w http.ResponseWriter, r *http.Request) { + s.cfg.NotFound(w, r) +} + +type notAllowedParams struct { + allowedMethods string + allowedHeaders map[string]string + acceptPost string + acceptPatch string +} + +func (s baseServer) notAllowed(w http.ResponseWriter, r *http.Request, params notAllowedParams) { + h := w.Header() + isOptions := r.Method == "OPTIONS" + if isOptions { + h.Set("Access-Control-Allow-Methods", params.allowedMethods) + if params.allowedHeaders != nil { + m := r.Header.Get("Access-Control-Request-Method") + if m != "" { + allowedHeaders, ok := params.allowedHeaders[strings.ToUpper(m)] + if ok { + h.Set("Access-Control-Allow-Headers", allowedHeaders) + } + } + } + if params.acceptPost != "" { + h.Set("Accept-Post", params.acceptPost) + } + if params.acceptPatch != "" { + h.Set("Accept-Patch", params.acceptPatch) + } + } + if s.cfg.MethodNotAllowed != nil { + s.cfg.MethodNotAllowed(w, r, params.allowedMethods) + return + } + status := http.StatusNoContent + if !isOptions { + h.Set("Allow", params.allowedMethods) + status = http.StatusMethodNotAllowed + } + w.WriteHeader(status) +} + +func (cfg serverConfig) baseServer() (s baseServer, err error) { + s = baseServer{cfg: cfg} + if s.requests, err = otelogen.ServerRequestCountCounter(s.cfg.Meter); err != nil { + return s, err + } + if s.errors, err = otelogen.ServerErrorsCountCounter(s.cfg.Meter); err != nil { + return s, err + } + if s.duration, err = otelogen.ServerDurationHistogram(s.cfg.Meter); err != nil { + return s, err + } + return s, nil +} + +type clientConfig struct { + otelConfig + Client ht.Client +} + +// ClientOption is client config option. +type ClientOption interface { + applyClient(*clientConfig) +} + +var _ ClientOption = (optionFunc[clientConfig])(nil) + +func (o optionFunc[C]) applyClient(c *C) { + o(c) +} + +var _ ClientOption = (otelOptionFunc)(nil) + +func (o otelOptionFunc) applyClient(c *clientConfig) { + o(&c.otelConfig) +} + +func newClientConfig(opts ...ClientOption) clientConfig { + cfg := clientConfig{ + Client: http.DefaultClient, + } + for _, opt := range opts { + opt.applyClient(&cfg) + } + cfg.initOTEL() + return cfg +} + +type baseClient struct { + cfg clientConfig + requests metric.Int64Counter + errors metric.Int64Counter + duration metric.Float64Histogram +} + +func (cfg clientConfig) baseClient() (c baseClient, err error) { + c = baseClient{cfg: cfg} + if c.requests, err = otelogen.ClientRequestCountCounter(c.cfg.Meter); err != nil { + return c, err + } + if c.errors, err = otelogen.ClientErrorsCountCounter(c.cfg.Meter); err != nil { + return c, err + } + if c.duration, err = otelogen.ClientDurationHistogram(c.cfg.Meter); err != nil { + return c, err + } + return c, nil +} + +// Option is config option. +type Option interface { + ServerOption + ClientOption +} + +// WithTracerProvider specifies a tracer provider to use for creating a tracer. +// +// If none is specified, the global provider is used. +func WithTracerProvider(provider trace.TracerProvider) Option { + return otelOptionFunc(func(cfg *otelConfig) { + if provider != nil { + cfg.TracerProvider = provider + } + }) +} + +// WithMeterProvider specifies a meter provider to use for creating a meter. +// +// If none is specified, the otel.GetMeterProvider() is used. +func WithMeterProvider(provider metric.MeterProvider) Option { + return otelOptionFunc(func(cfg *otelConfig) { + if provider != nil { + cfg.MeterProvider = provider + } + }) +} + +// WithAttributes specifies default otel attributes. +func WithAttributes(attributes ...attribute.KeyValue) Option { + return otelOptionFunc(func(cfg *otelConfig) { + cfg.Attributes = attributes + }) +} + +// WithClient specifies http client to use. +func WithClient(client ht.Client) ClientOption { + return optionFunc[clientConfig](func(cfg *clientConfig) { + if client != nil { + cfg.Client = client + } + }) +} + +// WithNotFound specifies Not Found handler to use. +func WithNotFound(notFound http.HandlerFunc) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if notFound != nil { + cfg.NotFound = notFound + } + }) +} + +// WithMethodNotAllowed specifies Method Not Allowed handler to use. +func WithMethodNotAllowed(methodNotAllowed func(w http.ResponseWriter, r *http.Request, allowed string)) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if methodNotAllowed != nil { + cfg.MethodNotAllowed = methodNotAllowed + } + }) +} + +// WithErrorHandler specifies error handler to use. +func WithErrorHandler(h ErrorHandler) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if h != nil { + cfg.ErrorHandler = h + } + }) +} + +// WithPathPrefix specifies server path prefix. +func WithPathPrefix(prefix string) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + cfg.Prefix = prefix + }) +} + +// WithMiddleware specifies middlewares to use. +func WithMiddleware(m ...Middleware) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + switch len(m) { + case 0: + cfg.Middleware = nil + case 1: + cfg.Middleware = m[0] + default: + cfg.Middleware = middleware.ChainMiddlewares(m...) + } + }) +} + +// WithMaxMultipartMemory specifies limit of memory for storing file parts. +// File parts which can't be stored in memory will be stored on disk in temporary files. +func WithMaxMultipartMemory(max int64) ServerOption { + return optionFunc[serverConfig](func(cfg *serverConfig) { + if max > 0 { + cfg.MaxMultipartMemory = max + } + }) +} diff --git a/api/rest/oas_client_gen.go b/api/rest/oas_client_gen.go new file mode 100644 index 0000000..b180041 --- /dev/null +++ b/api/rest/oas_client_gen.go @@ -0,0 +1,742 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "context" + "net/url" + "strings" + "time" + + "github.com/go-faster/errors" + "github.com/ogen-go/ogen/conv" + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/otelogen" + "github.com/ogen-go/ogen/uri" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + "go.opentelemetry.io/otel/trace" +) + +func trimTrailingSlashes(u *url.URL) { + u.Path = strings.TrimRight(u.Path, "/") + u.RawPath = strings.TrimRight(u.RawPath, "/") +} + +// Invoker invokes operations described by OpenAPI v3 specification. +type Invoker interface { + // GetEthereumNow invokes getEthereumNow operation. + // + // Get the current wall-clock slot and epoch. + // + // GET /api/v1/ethereum/now + GetEthereumNow(ctx context.Context) (GetEthereumNowRes, error) + // GetEthereumSpec invokes getEthereumSpec operation. + // + // Get the Ethereum network name and spec. + // + // GET /api/v1/ethereum/spec + GetEthereumSpec(ctx context.Context) (GetEthereumSpecRes, error) + // GetFrame invokes getFrame operation. + // + // Get a single fork-choice frame by ID. + // + // GET /api/v1/frames/{id} + GetFrame(ctx context.Context, params GetFrameParams) (GetFrameRes, error) + // ListMetadata invokes listMetadata operation. + // + // List fork-choice frame metadata. + // + // POST /api/v1/metadata + ListMetadata(ctx context.Context, request *MetadataQuery) (ListMetadataRes, error) + // ListMetadataEpochs invokes listMetadataEpochs operation. + // + // List distinct epochs across stored frames. + // + // POST /api/v1/metadata/epochs + ListMetadataEpochs(ctx context.Context, request *MetadataQuery) (ListMetadataEpochsRes, error) + // ListMetadataLabels invokes listMetadataLabels operation. + // + // List distinct labels across stored frames. + // + // POST /api/v1/metadata/labels + ListMetadataLabels(ctx context.Context, request *MetadataQuery) (ListMetadataLabelsRes, error) + // ListMetadataNodes invokes listMetadataNodes operation. + // + // List distinct nodes across stored frames. + // + // POST /api/v1/metadata/nodes + ListMetadataNodes(ctx context.Context, request *MetadataQuery) (ListMetadataNodesRes, error) + // ListMetadataSlots invokes listMetadataSlots operation. + // + // List distinct slots across stored frames. + // + // POST /api/v1/metadata/slots + ListMetadataSlots(ctx context.Context, request *MetadataQuery) (ListMetadataSlotsRes, error) +} + +// Client implements OAS client. +type Client struct { + serverURL *url.URL + baseClient +} + +// NewClient initializes new Client defined by OAS. +func NewClient(serverURL string, opts ...ClientOption) (*Client, error) { + u, err := url.Parse(serverURL) + if err != nil { + return nil, err + } + trimTrailingSlashes(u) + + c, err := newClientConfig(opts...).baseClient() + if err != nil { + return nil, err + } + return &Client{ + serverURL: u, + baseClient: c, + }, nil +} + +type serverURLKey struct{} + +// WithServerURL sets context key to override server URL. +func WithServerURL(ctx context.Context, u *url.URL) context.Context { + return context.WithValue(ctx, serverURLKey{}, u) +} + +func (c *Client) requestURL(ctx context.Context) *url.URL { + u, ok := ctx.Value(serverURLKey{}).(*url.URL) + if !ok { + return c.serverURL + } + return u +} + +// GetEthereumNow invokes getEthereumNow operation. +// +// Get the current wall-clock slot and epoch. +// +// GET /api/v1/ethereum/now +func (c *Client) GetEthereumNow(ctx context.Context) (GetEthereumNowRes, error) { + res, err := c.sendGetEthereumNow(ctx) + return res, err +} + +func (c *Client) sendGetEthereumNow(ctx context.Context) (res GetEthereumNowRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getEthereumNow"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.URLTemplateKey.String("/api/v1/ethereum/now"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, GetEthereumNowOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/ethereum/now" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeGetEthereumNowResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// GetEthereumSpec invokes getEthereumSpec operation. +// +// Get the Ethereum network name and spec. +// +// GET /api/v1/ethereum/spec +func (c *Client) GetEthereumSpec(ctx context.Context) (GetEthereumSpecRes, error) { + res, err := c.sendGetEthereumSpec(ctx) + return res, err +} + +func (c *Client) sendGetEthereumSpec(ctx context.Context) (res GetEthereumSpecRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getEthereumSpec"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.URLTemplateKey.String("/api/v1/ethereum/spec"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, GetEthereumSpecOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/ethereum/spec" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeGetEthereumSpecResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// GetFrame invokes getFrame operation. +// +// Get a single fork-choice frame by ID. +// +// GET /api/v1/frames/{id} +func (c *Client) GetFrame(ctx context.Context, params GetFrameParams) (GetFrameRes, error) { + res, err := c.sendGetFrame(ctx, params) + return res, err +} + +func (c *Client) sendGetFrame(ctx context.Context, params GetFrameParams) (res GetFrameRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getFrame"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.URLTemplateKey.String("/api/v1/frames/{id}"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, GetFrameOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [2]string + pathParts[0] = "/api/v1/frames/" + { + // Encode "id" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "id", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.StringToString(params.ID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "GET", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeGetFrameResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// ListMetadata invokes listMetadata operation. +// +// List fork-choice frame metadata. +// +// POST /api/v1/metadata +func (c *Client) ListMetadata(ctx context.Context, request *MetadataQuery) (ListMetadataRes, error) { + res, err := c.sendListMetadata(ctx, request) + return res, err +} + +func (c *Client) sendListMetadata(ctx context.Context, request *MetadataQuery) (res ListMetadataRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadata"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/api/v1/metadata"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ListMetadataOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/metadata" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeListMetadataRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeListMetadataResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// ListMetadataEpochs invokes listMetadataEpochs operation. +// +// List distinct epochs across stored frames. +// +// POST /api/v1/metadata/epochs +func (c *Client) ListMetadataEpochs(ctx context.Context, request *MetadataQuery) (ListMetadataEpochsRes, error) { + res, err := c.sendListMetadataEpochs(ctx, request) + return res, err +} + +func (c *Client) sendListMetadataEpochs(ctx context.Context, request *MetadataQuery) (res ListMetadataEpochsRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataEpochs"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/api/v1/metadata/epochs"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ListMetadataEpochsOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/metadata/epochs" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeListMetadataEpochsRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeListMetadataEpochsResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// ListMetadataLabels invokes listMetadataLabels operation. +// +// List distinct labels across stored frames. +// +// POST /api/v1/metadata/labels +func (c *Client) ListMetadataLabels(ctx context.Context, request *MetadataQuery) (ListMetadataLabelsRes, error) { + res, err := c.sendListMetadataLabels(ctx, request) + return res, err +} + +func (c *Client) sendListMetadataLabels(ctx context.Context, request *MetadataQuery) (res ListMetadataLabelsRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataLabels"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/api/v1/metadata/labels"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ListMetadataLabelsOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/metadata/labels" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeListMetadataLabelsRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeListMetadataLabelsResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// ListMetadataNodes invokes listMetadataNodes operation. +// +// List distinct nodes across stored frames. +// +// POST /api/v1/metadata/nodes +func (c *Client) ListMetadataNodes(ctx context.Context, request *MetadataQuery) (ListMetadataNodesRes, error) { + res, err := c.sendListMetadataNodes(ctx, request) + return res, err +} + +func (c *Client) sendListMetadataNodes(ctx context.Context, request *MetadataQuery) (res ListMetadataNodesRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataNodes"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/api/v1/metadata/nodes"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ListMetadataNodesOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/metadata/nodes" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeListMetadataNodesRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeListMetadataNodesResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + +// ListMetadataSlots invokes listMetadataSlots operation. +// +// List distinct slots across stored frames. +// +// POST /api/v1/metadata/slots +func (c *Client) ListMetadataSlots(ctx context.Context, request *MetadataQuery) (ListMetadataSlotsRes, error) { + res, err := c.sendListMetadataSlots(ctx, request) + return res, err +} + +func (c *Client) sendListMetadataSlots(ctx context.Context, request *MetadataQuery) (res ListMetadataSlotsRes, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataSlots"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/api/v1/metadata/slots"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, ListMetadataSlotsOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/api/v1/metadata/slots" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeListMetadataSlotsRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeListMetadataSlotsResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} diff --git a/api/rest/oas_defaults_gen.go b/api/rest/oas_defaults_gen.go new file mode 100644 index 0000000..9f9b5ee --- /dev/null +++ b/api/rest/oas_defaults_gen.go @@ -0,0 +1,15 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +// setDefaults set default value of fields. +func (s *PaginationCursor) setDefaults() { + { + val := int(0) + s.Offset.SetTo(val) + } + { + val := int(1000) + s.Limit.SetTo(val) + } +} diff --git a/api/rest/oas_handlers_gen.go b/api/rest/oas_handlers_gen.go new file mode 100644 index 0000000..01277a6 --- /dev/null +++ b/api/rest/oas_handlers_gen.go @@ -0,0 +1,1228 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "context" + "net/http" + "time" + + "github.com/go-faster/errors" + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/otelogen" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + "go.opentelemetry.io/otel/trace" +) + +type codeRecorder struct { + http.ResponseWriter + status int +} + +func (c *codeRecorder) WriteHeader(status int) { + c.status = status + c.ResponseWriter.WriteHeader(status) +} + +func (c *codeRecorder) Unwrap() http.ResponseWriter { + return c.ResponseWriter +} + +// handleGetEthereumNowRequest handles getEthereumNow operation. +// +// Get the current wall-clock slot and epoch. +// +// GET /api/v1/ethereum/now +func (s *Server) handleGetEthereumNowRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getEthereumNow"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/api/v1/ethereum/now"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), GetEthereumNowOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + ) + + var rawBody []byte + + var response GetEthereumNowRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetEthereumNowOperation, + OperationSummary: "Get the current wall-clock slot and epoch", + OperationID: "getEthereumNow", + Body: nil, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = GetEthereumNowRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetEthereumNow(ctx) + return response, err + }, + ) + } else { + response, err = s.h.GetEthereumNow(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetEthereumNowResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetEthereumSpecRequest handles getEthereumSpec operation. +// +// Get the Ethereum network name and spec. +// +// GET /api/v1/ethereum/spec +func (s *Server) handleGetEthereumSpecRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getEthereumSpec"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/api/v1/ethereum/spec"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), GetEthereumSpecOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + ) + + var rawBody []byte + + var response GetEthereumSpecRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetEthereumSpecOperation, + OperationSummary: "Get the Ethereum network name and spec", + OperationID: "getEthereumSpec", + Body: nil, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = struct{} + Params = struct{} + Response = GetEthereumSpecRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetEthereumSpec(ctx) + return response, err + }, + ) + } else { + response, err = s.h.GetEthereumSpec(ctx) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetEthereumSpecResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleGetFrameRequest handles getFrame operation. +// +// Get a single fork-choice frame by ID. +// +// GET /api/v1/frames/{id} +func (s *Server) handleGetFrameRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("getFrame"), + semconv.HTTPRequestMethodKey.String("GET"), + semconv.HTTPRouteKey.String("/api/v1/frames/{id}"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), GetFrameOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: GetFrameOperation, + ID: "getFrame", + } + ) + params, err := decodeGetFrameParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var rawBody []byte + + var response GetFrameRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: GetFrameOperation, + OperationSummary: "Get a single fork-choice frame by ID", + OperationID: "getFrame", + Body: nil, + RawBody: rawBody, + Params: middleware.Parameters{ + { + Name: "id", + In: "path", + }: params.ID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = GetFrameParams + Response = GetFrameRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackGetFrameParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.GetFrame(ctx, params) + return response, err + }, + ) + } else { + response, err = s.h.GetFrame(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeGetFrameResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListMetadataRequest handles listMetadata operation. +// +// List fork-choice frame metadata. +// +// POST /api/v1/metadata +func (s *Server) handleListMetadataRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadata"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/api/v1/metadata"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ListMetadataOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListMetadataOperation, + ID: "listMetadata", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodeListMetadataRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response ListMetadataRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListMetadataOperation, + OperationSummary: "List fork-choice frame metadata", + OperationID: "listMetadata", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *MetadataQuery + Params = struct{} + Response = ListMetadataRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListMetadata(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.ListMetadata(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListMetadataResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListMetadataEpochsRequest handles listMetadataEpochs operation. +// +// List distinct epochs across stored frames. +// +// POST /api/v1/metadata/epochs +func (s *Server) handleListMetadataEpochsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataEpochs"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/api/v1/metadata/epochs"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ListMetadataEpochsOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListMetadataEpochsOperation, + ID: "listMetadataEpochs", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodeListMetadataEpochsRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response ListMetadataEpochsRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListMetadataEpochsOperation, + OperationSummary: "List distinct epochs across stored frames", + OperationID: "listMetadataEpochs", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *MetadataQuery + Params = struct{} + Response = ListMetadataEpochsRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListMetadataEpochs(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.ListMetadataEpochs(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListMetadataEpochsResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListMetadataLabelsRequest handles listMetadataLabels operation. +// +// List distinct labels across stored frames. +// +// POST /api/v1/metadata/labels +func (s *Server) handleListMetadataLabelsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataLabels"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/api/v1/metadata/labels"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ListMetadataLabelsOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListMetadataLabelsOperation, + ID: "listMetadataLabels", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodeListMetadataLabelsRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response ListMetadataLabelsRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListMetadataLabelsOperation, + OperationSummary: "List distinct labels across stored frames", + OperationID: "listMetadataLabels", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *MetadataQuery + Params = struct{} + Response = ListMetadataLabelsRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListMetadataLabels(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.ListMetadataLabels(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListMetadataLabelsResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListMetadataNodesRequest handles listMetadataNodes operation. +// +// List distinct nodes across stored frames. +// +// POST /api/v1/metadata/nodes +func (s *Server) handleListMetadataNodesRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataNodes"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/api/v1/metadata/nodes"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ListMetadataNodesOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListMetadataNodesOperation, + ID: "listMetadataNodes", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodeListMetadataNodesRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response ListMetadataNodesRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListMetadataNodesOperation, + OperationSummary: "List distinct nodes across stored frames", + OperationID: "listMetadataNodes", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *MetadataQuery + Params = struct{} + Response = ListMetadataNodesRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListMetadataNodes(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.ListMetadataNodes(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListMetadataNodesResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + +// handleListMetadataSlotsRequest handles listMetadataSlots operation. +// +// List distinct slots across stored frames. +// +// POST /api/v1/metadata/slots +func (s *Server) handleListMetadataSlotsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("listMetadataSlots"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/api/v1/metadata/slots"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), ListMetadataSlotsOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: ListMetadataSlotsOperation, + ID: "listMetadataSlots", + } + ) + + var rawBody []byte + request, rawBody, close, err := s.decodeListMetadataSlotsRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response ListMetadataSlotsRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: ListMetadataSlotsOperation, + OperationSummary: "List distinct slots across stored frames", + OperationID: "listMetadataSlots", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = *MetadataQuery + Params = struct{} + Response = ListMetadataSlotsRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.ListMetadataSlots(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.ListMetadataSlots(ctx, request) + } + if err != nil { + if errRes, ok := errors.Into[*UnexpectedErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeListMetadataSlotsResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} diff --git a/api/rest/oas_interfaces_gen.go b/api/rest/oas_interfaces_gen.go new file mode 100644 index 0000000..f786f4a --- /dev/null +++ b/api/rest/oas_interfaces_gen.go @@ -0,0 +1,34 @@ +// Code generated by ogen, DO NOT EDIT. +package rest + +type GetEthereumNowRes interface { + getEthereumNowRes() +} + +type GetEthereumSpecRes interface { + getEthereumSpecRes() +} + +type GetFrameRes interface { + getFrameRes() +} + +type ListMetadataEpochsRes interface { + listMetadataEpochsRes() +} + +type ListMetadataLabelsRes interface { + listMetadataLabelsRes() +} + +type ListMetadataNodesRes interface { + listMetadataNodesRes() +} + +type ListMetadataRes interface { + listMetadataRes() +} + +type ListMetadataSlotsRes interface { + listMetadataSlotsRes() +} diff --git a/api/rest/oas_json_gen.go b/api/rest/oas_json_gen.go new file mode 100644 index 0000000..33952ec --- /dev/null +++ b/api/rest/oas_json_gen.go @@ -0,0 +1,3753 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "math/bits" + "strconv" + "time" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "github.com/ogen-go/ogen/json" + "github.com/ogen-go/ogen/validate" +) + +// Encode implements json.Marshaler. +func (s *Checkpoint) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *Checkpoint) encodeFields(e *jx.Encoder) { + { + e.FieldStart("epoch") + s.Epoch.Encode(e) + } + { + e.FieldStart("root") + s.Root.Encode(e) + } +} + +var jsonFieldsNameOfCheckpoint = [2]string{ + 0: "epoch", + 1: "root", +} + +// Decode decodes Checkpoint from json. +func (s *Checkpoint) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Checkpoint to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "epoch": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Epoch.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"epoch\"") + } + case "root": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Root.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"root\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode Checkpoint") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfCheckpoint) { + name = jsonFieldsNameOfCheckpoint[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *Checkpoint) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Checkpoint) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Epoch as json. +func (s Epoch) Encode(e *jx.Encoder) { + unwrapped := int64(s) + + e.Int64(unwrapped) +} + +// Decode decodes Epoch from json. +func (s *Epoch) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Epoch to nil") + } + var unwrapped int64 + if err := func() error { + v, err := d.Int64() + unwrapped = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = Epoch(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s Epoch) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Epoch) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes EpochString as json. +func (s EpochString) Encode(e *jx.Encoder) { + unwrapped := string(s) + + e.Str(unwrapped) +} + +// Decode decodes EpochString from json. +func (s *EpochString) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode EpochString to nil") + } + var unwrapped string + if err := func() error { + v, err := d.Str() + unwrapped = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = EpochString(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s EpochString) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *EpochString) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *Error) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *Error) encodeFields(e *jx.Encoder) { + { + e.FieldStart("code") + e.Int(s.Code) + } + { + e.FieldStart("message") + e.Str(s.Message) + } +} + +var jsonFieldsNameOfError = [2]string{ + 0: "code", + 1: "message", +} + +// Decode decodes Error from json. +func (s *Error) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Error to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "code": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Int() + s.Code = int(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"code\"") + } + case "message": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Message = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"message\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode Error") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfError) { + name = jsonFieldsNameOfError[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *Error) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Error) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *EthereumNow) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *EthereumNow) encodeFields(e *jx.Encoder) { + { + e.FieldStart("slot") + s.Slot.Encode(e) + } + { + e.FieldStart("epoch") + s.Epoch.Encode(e) + } +} + +var jsonFieldsNameOfEthereumNow = [2]string{ + 0: "slot", + 1: "epoch", +} + +// Decode decodes EthereumNow from json. +func (s *EthereumNow) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode EthereumNow to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "slot": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Slot.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"slot\"") + } + case "epoch": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Epoch.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"epoch\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode EthereumNow") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfEthereumNow) { + name = jsonFieldsNameOfEthereumNow[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *EthereumNow) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *EthereumNow) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *EthereumSpec) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *EthereumSpec) encodeFields(e *jx.Encoder) { + { + e.FieldStart("seconds_per_slot") + e.Int64(s.SecondsPerSlot) + } + { + e.FieldStart("slots_per_epoch") + e.Int64(s.SlotsPerEpoch) + } + { + e.FieldStart("genesis_time") + json.EncodeDateTime(e, s.GenesisTime) + } +} + +var jsonFieldsNameOfEthereumSpec = [3]string{ + 0: "seconds_per_slot", + 1: "slots_per_epoch", + 2: "genesis_time", +} + +// Decode decodes EthereumSpec from json. +func (s *EthereumSpec) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode EthereumSpec to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "seconds_per_slot": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Int64() + s.SecondsPerSlot = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"seconds_per_slot\"") + } + case "slots_per_epoch": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Int64() + s.SlotsPerEpoch = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"slots_per_epoch\"") + } + case "genesis_time": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := json.DecodeDateTime(d) + s.GenesisTime = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"genesis_time\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode EthereumSpec") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfEthereumSpec) { + name = jsonFieldsNameOfEthereumSpec[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *EthereumSpec) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *EthereumSpec) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *EthereumSpecResult) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *EthereumSpecResult) encodeFields(e *jx.Encoder) { + { + e.FieldStart("network_name") + e.Str(s.NetworkName) + } + { + e.FieldStart("spec") + s.Spec.Encode(e) + } +} + +var jsonFieldsNameOfEthereumSpecResult = [2]string{ + 0: "network_name", + 1: "spec", +} + +// Decode decodes EthereumSpecResult from json. +func (s *EthereumSpecResult) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode EthereumSpecResult to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "network_name": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.NetworkName = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"network_name\"") + } + case "spec": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Spec.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"spec\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode EthereumSpecResult") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfEthereumSpecResult) { + name = jsonFieldsNameOfEthereumSpecResult[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *EthereumSpecResult) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *EthereumSpecResult) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ForkChoice) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ForkChoice) encodeFields(e *jx.Encoder) { + { + e.FieldStart("justified_checkpoint") + s.JustifiedCheckpoint.Encode(e) + } + { + e.FieldStart("finalized_checkpoint") + s.FinalizedCheckpoint.Encode(e) + } + { + e.FieldStart("fork_choice_nodes") + e.ArrStart() + for _, elem := range s.ForkChoiceNodes { + elem.Encode(e) + } + e.ArrEnd() + } +} + +var jsonFieldsNameOfForkChoice = [3]string{ + 0: "justified_checkpoint", + 1: "finalized_checkpoint", + 2: "fork_choice_nodes", +} + +// Decode decodes ForkChoice from json. +func (s *ForkChoice) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ForkChoice to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "justified_checkpoint": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.JustifiedCheckpoint.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"justified_checkpoint\"") + } + case "finalized_checkpoint": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.FinalizedCheckpoint.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"finalized_checkpoint\"") + } + case "fork_choice_nodes": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + s.ForkChoiceNodes = make([]ForkChoiceNode, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem ForkChoiceNode + if err := elem.Decode(d); err != nil { + return err + } + s.ForkChoiceNodes = append(s.ForkChoiceNodes, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"fork_choice_nodes\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ForkChoice") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfForkChoice) { + name = jsonFieldsNameOfForkChoice[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ForkChoice) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ForkChoice) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ForkChoiceNode) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ForkChoiceNode) encodeFields(e *jx.Encoder) { + { + e.FieldStart("slot") + s.Slot.Encode(e) + } + { + e.FieldStart("block_root") + s.BlockRoot.Encode(e) + } + { + e.FieldStart("parent_root") + s.ParentRoot.Encode(e) + } + { + e.FieldStart("justified_epoch") + s.JustifiedEpoch.Encode(e) + } + { + e.FieldStart("finalized_epoch") + s.FinalizedEpoch.Encode(e) + } + { + e.FieldStart("weight") + e.Str(s.Weight) + } + { + e.FieldStart("validity") + s.Validity.Encode(e) + } + { + e.FieldStart("execution_block_hash") + s.ExecutionBlockHash.Encode(e) + } + { + if s.ExtraData.Set { + e.FieldStart("extra_data") + s.ExtraData.Encode(e) + } + } +} + +var jsonFieldsNameOfForkChoiceNode = [9]string{ + 0: "slot", + 1: "block_root", + 2: "parent_root", + 3: "justified_epoch", + 4: "finalized_epoch", + 5: "weight", + 6: "validity", + 7: "execution_block_hash", + 8: "extra_data", +} + +// Decode decodes ForkChoiceNode from json. +func (s *ForkChoiceNode) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ForkChoiceNode to nil") + } + var requiredBitSet [2]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "slot": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Slot.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"slot\"") + } + case "block_root": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.BlockRoot.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"block_root\"") + } + case "parent_root": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.ParentRoot.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"parent_root\"") + } + case "justified_epoch": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + if err := s.JustifiedEpoch.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"justified_epoch\"") + } + case "finalized_epoch": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + if err := s.FinalizedEpoch.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"finalized_epoch\"") + } + case "weight": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + v, err := d.Str() + s.Weight = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"weight\"") + } + case "validity": + requiredBitSet[0] |= 1 << 6 + if err := func() error { + if err := s.Validity.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"validity\"") + } + case "execution_block_hash": + requiredBitSet[0] |= 1 << 7 + if err := func() error { + if err := s.ExecutionBlockHash.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"execution_block_hash\"") + } + case "extra_data": + if err := func() error { + s.ExtraData.Reset() + if err := s.ExtraData.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"extra_data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ForkChoiceNode") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [2]uint8{ + 0b11111111, + 0b00000000, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfForkChoiceNode) { + name = jsonFieldsNameOfForkChoiceNode[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ForkChoiceNode) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ForkChoiceNode) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s ForkChoiceNodeExtraData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields implements json.Marshaler. +func (s ForkChoiceNodeExtraData) encodeFields(e *jx.Encoder) { + for k, elem := range s { + e.FieldStart(k) + + if len(elem) != 0 { + e.Raw(elem) + } + } +} + +// Decode decodes ForkChoiceNodeExtraData from json. +func (s *ForkChoiceNodeExtraData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ForkChoiceNodeExtraData to nil") + } + m := s.init() + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + var elem jx.Raw + if err := func() error { + v, err := d.RawAppend(nil) + elem = jx.Raw(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrapf(err, "decode field %q", k) + } + m[string(k)] = elem + return nil + }); err != nil { + return errors.Wrap(err, "decode ForkChoiceNodeExtraData") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s ForkChoiceNodeExtraData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ForkChoiceNodeExtraData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes ForkChoiceNodeValidity as json. +func (s ForkChoiceNodeValidity) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes ForkChoiceNodeValidity from json. +func (s *ForkChoiceNodeValidity) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ForkChoiceNodeValidity to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch ForkChoiceNodeValidity(v) { + case ForkChoiceNodeValidityValid: + *s = ForkChoiceNodeValidityValid + case ForkChoiceNodeValidityInvalid: + *s = ForkChoiceNodeValidityInvalid + case ForkChoiceNodeValidityOptimistic: + *s = ForkChoiceNodeValidityOptimistic + case ForkChoiceNodeValidityUnknown: + *s = ForkChoiceNodeValidityUnknown + default: + *s = ForkChoiceNodeValidity(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s ForkChoiceNodeValidity) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ForkChoiceNodeValidity) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *Frame) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *Frame) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } + { + e.FieldStart("metadata") + s.Metadata.Encode(e) + } +} + +var jsonFieldsNameOfFrame = [2]string{ + 0: "data", + 1: "metadata", +} + +// Decode decodes Frame from json. +func (s *Frame) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Frame to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + case "metadata": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Metadata.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"metadata\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode Frame") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfFrame) { + name = jsonFieldsNameOfFrame[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *Frame) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Frame) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *FrameFilter) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *FrameFilter) encodeFields(e *jx.Encoder) { + { + if s.Node.Set { + e.FieldStart("node") + s.Node.Encode(e) + } + } + { + if s.Before.Set { + e.FieldStart("before") + s.Before.Encode(e, json.EncodeDateTime) + } + } + { + if s.After.Set { + e.FieldStart("after") + s.After.Encode(e, json.EncodeDateTime) + } + } + { + if s.Slot.Set { + e.FieldStart("slot") + s.Slot.Encode(e) + } + } + { + if s.Epoch.Set { + e.FieldStart("epoch") + s.Epoch.Encode(e) + } + } + { + if s.Labels != nil { + e.FieldStart("labels") + e.ArrStart() + for _, elem := range s.Labels { + e.Str(elem) + } + e.ArrEnd() + } + } + { + if s.ConsensusClient.Set { + e.FieldStart("consensus_client") + s.ConsensusClient.Encode(e) + } + } + { + if s.EventSource.Set { + e.FieldStart("event_source") + s.EventSource.Encode(e) + } + } +} + +var jsonFieldsNameOfFrameFilter = [8]string{ + 0: "node", + 1: "before", + 2: "after", + 3: "slot", + 4: "epoch", + 5: "labels", + 6: "consensus_client", + 7: "event_source", +} + +// Decode decodes FrameFilter from json. +func (s *FrameFilter) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode FrameFilter to nil") + } + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "node": + if err := func() error { + s.Node.Reset() + if err := s.Node.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"node\"") + } + case "before": + if err := func() error { + s.Before.Reset() + if err := s.Before.Decode(d, json.DecodeDateTime); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"before\"") + } + case "after": + if err := func() error { + s.After.Reset() + if err := s.After.Decode(d, json.DecodeDateTime); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"after\"") + } + case "slot": + if err := func() error { + s.Slot.Reset() + if err := s.Slot.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"slot\"") + } + case "epoch": + if err := func() error { + s.Epoch.Reset() + if err := s.Epoch.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"epoch\"") + } + case "labels": + if err := func() error { + s.Labels = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Labels = append(s.Labels, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"labels\"") + } + case "consensus_client": + if err := func() error { + s.ConsensusClient.Reset() + if err := s.ConsensusClient.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"consensus_client\"") + } + case "event_source": + if err := func() error { + s.EventSource.Reset() + if err := s.EventSource.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"event_source\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode FrameFilter") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *FrameFilter) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *FrameFilter) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *FrameMetadata) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *FrameMetadata) encodeFields(e *jx.Encoder) { + { + e.FieldStart("id") + e.Str(s.ID) + } + { + e.FieldStart("node") + e.Str(s.Node) + } + { + e.FieldStart("fetched_at") + json.EncodeDateTime(e, s.FetchedAt) + } + { + e.FieldStart("wall_clock_slot") + s.WallClockSlot.Encode(e) + } + { + e.FieldStart("wall_clock_epoch") + s.WallClockEpoch.Encode(e) + } + { + e.FieldStart("labels") + e.ArrStart() + for _, elem := range s.Labels { + e.Str(elem) + } + e.ArrEnd() + } + { + e.FieldStart("consensus_client") + e.Str(s.ConsensusClient) + } + { + e.FieldStart("event_source") + e.Str(s.EventSource) + } +} + +var jsonFieldsNameOfFrameMetadata = [8]string{ + 0: "id", + 1: "node", + 2: "fetched_at", + 3: "wall_clock_slot", + 4: "wall_clock_epoch", + 5: "labels", + 6: "consensus_client", + 7: "event_source", +} + +// Decode decodes FrameMetadata from json. +func (s *FrameMetadata) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode FrameMetadata to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "id": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Str() + s.ID = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"id\"") + } + case "node": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + v, err := d.Str() + s.Node = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"node\"") + } + case "fetched_at": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + v, err := json.DecodeDateTime(d) + s.FetchedAt = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"fetched_at\"") + } + case "wall_clock_slot": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + if err := s.WallClockSlot.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"wall_clock_slot\"") + } + case "wall_clock_epoch": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + if err := s.WallClockEpoch.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"wall_clock_epoch\"") + } + case "labels": + requiredBitSet[0] |= 1 << 5 + if err := func() error { + s.Labels = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Labels = append(s.Labels, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"labels\"") + } + case "consensus_client": + requiredBitSet[0] |= 1 << 6 + if err := func() error { + v, err := d.Str() + s.ConsensusClient = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"consensus_client\"") + } + case "event_source": + requiredBitSet[0] |= 1 << 7 + if err := func() error { + v, err := d.Str() + s.EventSource = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"event_source\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode FrameMetadata") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b11111111, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfFrameMetadata) { + name = jsonFieldsNameOfFrameMetadata[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *FrameMetadata) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *FrameMetadata) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *GetEthereumNowOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *GetEthereumNowOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfGetEthereumNowOK = [1]string{ + 0: "data", +} + +// Decode decodes GetEthereumNowOK from json. +func (s *GetEthereumNowOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode GetEthereumNowOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode GetEthereumNowOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfGetEthereumNowOK) { + name = jsonFieldsNameOfGetEthereumNowOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *GetEthereumNowOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *GetEthereumNowOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *GetEthereumSpecOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *GetEthereumSpecOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfGetEthereumSpecOK = [1]string{ + 0: "data", +} + +// Decode decodes GetEthereumSpecOK from json. +func (s *GetEthereumSpecOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode GetEthereumSpecOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode GetEthereumSpecOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfGetEthereumSpecOK) { + name = jsonFieldsNameOfGetEthereumSpecOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *GetEthereumSpecOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *GetEthereumSpecOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *GetFrameOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *GetFrameOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfGetFrameOK = [1]string{ + 0: "data", +} + +// Decode decodes GetFrameOK from json. +func (s *GetFrameOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode GetFrameOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode GetFrameOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfGetFrameOK) { + name = jsonFieldsNameOfGetFrameOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *GetFrameOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *GetFrameOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *GetFrameOKData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *GetFrameOKData) encodeFields(e *jx.Encoder) { + { + e.FieldStart("frame") + s.Frame.Encode(e) + } +} + +var jsonFieldsNameOfGetFrameOKData = [1]string{ + 0: "frame", +} + +// Decode decodes GetFrameOKData from json. +func (s *GetFrameOKData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode GetFrameOKData to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "frame": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Frame.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"frame\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode GetFrameOKData") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfGetFrameOKData) { + name = jsonFieldsNameOfGetFrameOKData[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *GetFrameOKData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *GetFrameOKData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataEpochsOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataEpochsOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataEpochsOK = [1]string{ + 0: "data", +} + +// Decode decodes ListMetadataEpochsOK from json. +func (s *ListMetadataEpochsOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataEpochsOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataEpochsOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataEpochsOK) { + name = jsonFieldsNameOfListMetadataEpochsOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataEpochsOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataEpochsOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataEpochsOKData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataEpochsOKData) encodeFields(e *jx.Encoder) { + { + e.FieldStart("epochs") + e.ArrStart() + for _, elem := range s.Epochs { + elem.Encode(e) + } + e.ArrEnd() + } + { + e.FieldStart("pagination") + s.Pagination.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataEpochsOKData = [2]string{ + 0: "epochs", + 1: "pagination", +} + +// Decode decodes ListMetadataEpochsOKData from json. +func (s *ListMetadataEpochsOKData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataEpochsOKData to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "epochs": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Epochs = make([]Epoch, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Epoch + if err := elem.Decode(d); err != nil { + return err + } + s.Epochs = append(s.Epochs, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"epochs\"") + } + case "pagination": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pagination.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pagination\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataEpochsOKData") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataEpochsOKData) { + name = jsonFieldsNameOfListMetadataEpochsOKData[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataEpochsOKData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataEpochsOKData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataLabelsOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataLabelsOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataLabelsOK = [1]string{ + 0: "data", +} + +// Decode decodes ListMetadataLabelsOK from json. +func (s *ListMetadataLabelsOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataLabelsOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataLabelsOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataLabelsOK) { + name = jsonFieldsNameOfListMetadataLabelsOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataLabelsOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataLabelsOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataLabelsOKData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataLabelsOKData) encodeFields(e *jx.Encoder) { + { + e.FieldStart("labels") + e.ArrStart() + for _, elem := range s.Labels { + e.Str(elem) + } + e.ArrEnd() + } + { + e.FieldStart("pagination") + s.Pagination.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataLabelsOKData = [2]string{ + 0: "labels", + 1: "pagination", +} + +// Decode decodes ListMetadataLabelsOKData from json. +func (s *ListMetadataLabelsOKData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataLabelsOKData to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "labels": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Labels = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Labels = append(s.Labels, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"labels\"") + } + case "pagination": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pagination.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pagination\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataLabelsOKData") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataLabelsOKData) { + name = jsonFieldsNameOfListMetadataLabelsOKData[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataLabelsOKData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataLabelsOKData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataNodesOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataNodesOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataNodesOK = [1]string{ + 0: "data", +} + +// Decode decodes ListMetadataNodesOK from json. +func (s *ListMetadataNodesOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataNodesOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataNodesOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataNodesOK) { + name = jsonFieldsNameOfListMetadataNodesOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataNodesOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataNodesOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataNodesOKData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataNodesOKData) encodeFields(e *jx.Encoder) { + { + e.FieldStart("nodes") + e.ArrStart() + for _, elem := range s.Nodes { + e.Str(elem) + } + e.ArrEnd() + } + { + e.FieldStart("pagination") + s.Pagination.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataNodesOKData = [2]string{ + 0: "nodes", + 1: "pagination", +} + +// Decode decodes ListMetadataNodesOKData from json. +func (s *ListMetadataNodesOKData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataNodesOKData to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "nodes": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Nodes = make([]string, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem string + v, err := d.Str() + elem = string(v) + if err != nil { + return err + } + s.Nodes = append(s.Nodes, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"nodes\"") + } + case "pagination": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pagination.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pagination\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataNodesOKData") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataNodesOKData) { + name = jsonFieldsNameOfListMetadataNodesOKData[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataNodesOKData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataNodesOKData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataOK = [1]string{ + 0: "data", +} + +// Decode decodes ListMetadataOK from json. +func (s *ListMetadataOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataOK) { + name = jsonFieldsNameOfListMetadataOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataOKData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataOKData) encodeFields(e *jx.Encoder) { + { + e.FieldStart("frames") + e.ArrStart() + for _, elem := range s.Frames { + elem.Encode(e) + } + e.ArrEnd() + } + { + e.FieldStart("pagination") + s.Pagination.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataOKData = [2]string{ + 0: "frames", + 1: "pagination", +} + +// Decode decodes ListMetadataOKData from json. +func (s *ListMetadataOKData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataOKData to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "frames": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Frames = make([]FrameMetadata, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem FrameMetadata + if err := elem.Decode(d); err != nil { + return err + } + s.Frames = append(s.Frames, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"frames\"") + } + case "pagination": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pagination.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pagination\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataOKData") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataOKData) { + name = jsonFieldsNameOfListMetadataOKData[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataOKData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataOKData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataSlotsOK) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataSlotsOK) encodeFields(e *jx.Encoder) { + { + e.FieldStart("data") + s.Data.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataSlotsOK = [1]string{ + 0: "data", +} + +// Decode decodes ListMetadataSlotsOK from json. +func (s *ListMetadataSlotsOK) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataSlotsOK to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "data": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Data.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"data\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataSlotsOK") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataSlotsOK) { + name = jsonFieldsNameOfListMetadataSlotsOK[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataSlotsOK) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataSlotsOK) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *ListMetadataSlotsOKData) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *ListMetadataSlotsOKData) encodeFields(e *jx.Encoder) { + { + e.FieldStart("slots") + e.ArrStart() + for _, elem := range s.Slots { + elem.Encode(e) + } + e.ArrEnd() + } + { + e.FieldStart("pagination") + s.Pagination.Encode(e) + } +} + +var jsonFieldsNameOfListMetadataSlotsOKData = [2]string{ + 0: "slots", + 1: "pagination", +} + +// Decode decodes ListMetadataSlotsOKData from json. +func (s *ListMetadataSlotsOKData) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode ListMetadataSlotsOKData to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "slots": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Slots = make([]Slot, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem Slot + if err := elem.Decode(d); err != nil { + return err + } + s.Slots = append(s.Slots, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"slots\"") + } + case "pagination": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Pagination.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pagination\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ListMetadataSlotsOKData") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfListMetadataSlotsOKData) { + name = jsonFieldsNameOfListMetadataSlotsOKData[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ListMetadataSlotsOKData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ListMetadataSlotsOKData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *MetadataQuery) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *MetadataQuery) encodeFields(e *jx.Encoder) { + { + if s.Filter.Set { + e.FieldStart("filter") + s.Filter.Encode(e) + } + } + { + if s.Pagination.Set { + e.FieldStart("pagination") + s.Pagination.Encode(e) + } + } +} + +var jsonFieldsNameOfMetadataQuery = [2]string{ + 0: "filter", + 1: "pagination", +} + +// Decode decodes MetadataQuery from json. +func (s *MetadataQuery) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode MetadataQuery to nil") + } + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "filter": + if err := func() error { + s.Filter.Reset() + if err := s.Filter.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"filter\"") + } + case "pagination": + if err := func() error { + s.Pagination.Reset() + if err := s.Pagination.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"pagination\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode MetadataQuery") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *MetadataQuery) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *MetadataQuery) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes time.Time as json. +func (o OptDateTime) Encode(e *jx.Encoder, format func(*jx.Encoder, time.Time)) { + if !o.Set { + return + } + format(e, o.Value) +} + +// Decode decodes time.Time from json. +func (o *OptDateTime) Decode(d *jx.Decoder, format func(*jx.Decoder) (time.Time, error)) error { + if o == nil { + return errors.New("invalid: unable to decode OptDateTime to nil") + } + o.Set = true + v, err := format(d) + if err != nil { + return err + } + o.Value = v + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptDateTime) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e, json.EncodeDateTime) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptDateTime) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d, json.DecodeDateTime) +} + +// Encode encodes Epoch as json. +func (o OptEpoch) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes Epoch from json. +func (o *OptEpoch) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptEpoch to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptEpoch) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptEpoch) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes ForkChoiceNodeExtraData as json. +func (o OptForkChoiceNodeExtraData) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes ForkChoiceNodeExtraData from json. +func (o *OptForkChoiceNodeExtraData) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptForkChoiceNodeExtraData to nil") + } + o.Set = true + o.Value = make(ForkChoiceNodeExtraData) + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptForkChoiceNodeExtraData) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptForkChoiceNodeExtraData) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes FrameFilter as json. +func (o OptFrameFilter) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes FrameFilter from json. +func (o *OptFrameFilter) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptFrameFilter to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptFrameFilter) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptFrameFilter) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes int as json. +func (o OptInt) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Int(int(o.Value)) +} + +// Decode decodes int from json. +func (o *OptInt) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptInt to nil") + } + o.Set = true + v, err := d.Int() + if err != nil { + return err + } + o.Value = int(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptInt) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptInt) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes PaginationCursor as json. +func (o OptPaginationCursor) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes PaginationCursor from json. +func (o *OptPaginationCursor) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptPaginationCursor to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptPaginationCursor) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptPaginationCursor) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Slot as json. +func (o OptSlot) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes Slot from json. +func (o *OptSlot) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptSlot to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptSlot) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptSlot) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes string as json. +func (o OptString) Encode(e *jx.Encoder) { + if !o.Set { + return + } + e.Str(string(o.Value)) +} + +// Decode decodes string from json. +func (o *OptString) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptString to nil") + } + o.Set = true + v, err := d.Str() + if err != nil { + return err + } + o.Value = string(v) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptString) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptString) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *PaginationCursor) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PaginationCursor) encodeFields(e *jx.Encoder) { + { + if s.Offset.Set { + e.FieldStart("offset") + s.Offset.Encode(e) + } + } + { + if s.Limit.Set { + e.FieldStart("limit") + s.Limit.Encode(e) + } + } +} + +var jsonFieldsNameOfPaginationCursor = [2]string{ + 0: "offset", + 1: "limit", +} + +// Decode decodes PaginationCursor from json. +func (s *PaginationCursor) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PaginationCursor to nil") + } + s.setDefaults() + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "offset": + if err := func() error { + s.Offset.Reset() + if err := s.Offset.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"offset\"") + } + case "limit": + if err := func() error { + s.Limit.Reset() + if err := s.Limit.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"limit\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PaginationCursor") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PaginationCursor) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PaginationCursor) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *PaginationResponse) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *PaginationResponse) encodeFields(e *jx.Encoder) { + { + e.FieldStart("total") + e.Int64(s.Total) + } +} + +var jsonFieldsNameOfPaginationResponse = [1]string{ + 0: "total", +} + +// Decode decodes PaginationResponse from json. +func (s *PaginationResponse) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode PaginationResponse to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "total": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + v, err := d.Int64() + s.Total = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"total\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode PaginationResponse") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000001, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfPaginationResponse) { + name = jsonFieldsNameOfPaginationResponse[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *PaginationResponse) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *PaginationResponse) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Root as json. +func (s Root) Encode(e *jx.Encoder) { + unwrapped := string(s) + + e.Str(unwrapped) +} + +// Decode decodes Root from json. +func (s *Root) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Root to nil") + } + var unwrapped string + if err := func() error { + v, err := d.Str() + unwrapped = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = Root(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s Root) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Root) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes Slot as json. +func (s Slot) Encode(e *jx.Encoder) { + unwrapped := int64(s) + + e.Int64(unwrapped) +} + +// Decode decodes Slot from json. +func (s *Slot) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode Slot to nil") + } + var unwrapped int64 + if err := func() error { + v, err := d.Int64() + unwrapped = int64(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = Slot(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s Slot) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *Slot) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes SlotString as json. +func (s SlotString) Encode(e *jx.Encoder) { + unwrapped := string(s) + + e.Str(unwrapped) +} + +// Decode decodes SlotString from json. +func (s *SlotString) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode SlotString to nil") + } + var unwrapped string + if err := func() error { + v, err := d.Str() + unwrapped = string(v) + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = SlotString(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s SlotString) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *SlotString) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} diff --git a/api/rest/oas_labeler_gen.go b/api/rest/oas_labeler_gen.go new file mode 100644 index 0000000..b726eef --- /dev/null +++ b/api/rest/oas_labeler_gen.go @@ -0,0 +1,42 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" +) + +// Labeler is used to allow adding custom attributes to the server request metrics. +type Labeler struct { + attrs []attribute.KeyValue +} + +// Add attributes to the Labeler. +func (l *Labeler) Add(attrs ...attribute.KeyValue) { + l.attrs = append(l.attrs, attrs...) +} + +// AttributeSet returns the attributes added to the Labeler as an attribute.Set. +func (l *Labeler) AttributeSet() attribute.Set { + return attribute.NewSet(l.attrs...) +} + +type labelerContextKey struct{} + +// LabelerFromContext retrieves the Labeler from the provided context, if present. +// +// If no Labeler was found in the provided context a new, empty Labeler is returned and the second +// return value is false. In this case it is safe to use the Labeler but any attributes added to +// it will not be used. +func LabelerFromContext(ctx context.Context) (*Labeler, bool) { + if l, ok := ctx.Value(labelerContextKey{}).(*Labeler); ok { + return l, true + } + return &Labeler{}, false +} + +func contextWithLabeler(ctx context.Context, l *Labeler) context.Context { + return context.WithValue(ctx, labelerContextKey{}, l) +} diff --git a/api/rest/oas_middleware_gen.go b/api/rest/oas_middleware_gen.go new file mode 100644 index 0000000..57ea1b4 --- /dev/null +++ b/api/rest/oas_middleware_gen.go @@ -0,0 +1,10 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "github.com/ogen-go/ogen/middleware" +) + +// Middleware is middleware type. +type Middleware = middleware.Middleware diff --git a/api/rest/oas_operations_gen.go b/api/rest/oas_operations_gen.go new file mode 100644 index 0000000..8b3ad65 --- /dev/null +++ b/api/rest/oas_operations_gen.go @@ -0,0 +1,17 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +// OperationName is the ogen operation name +type OperationName = string + +const ( + GetEthereumNowOperation OperationName = "GetEthereumNow" + GetEthereumSpecOperation OperationName = "GetEthereumSpec" + GetFrameOperation OperationName = "GetFrame" + ListMetadataOperation OperationName = "ListMetadata" + ListMetadataEpochsOperation OperationName = "ListMetadataEpochs" + ListMetadataLabelsOperation OperationName = "ListMetadataLabels" + ListMetadataNodesOperation OperationName = "ListMetadataNodes" + ListMetadataSlotsOperation OperationName = "ListMetadataSlots" +) diff --git a/api/rest/oas_parameters_gen.go b/api/rest/oas_parameters_gen.go new file mode 100644 index 0000000..ff99d45 --- /dev/null +++ b/api/rest/oas_parameters_gen.go @@ -0,0 +1,81 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "net/http" + "net/url" + + "github.com/go-faster/errors" + "github.com/ogen-go/ogen/conv" + "github.com/ogen-go/ogen/middleware" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/uri" + "github.com/ogen-go/ogen/validate" +) + +// GetFrameParams is parameters of getFrame operation. +type GetFrameParams struct { + // The frame ID. + ID string +} + +func unpackGetFrameParams(packed middleware.Parameters) (params GetFrameParams) { + { + key := middleware.ParameterKey{ + Name: "id", + In: "path", + } + params.ID = packed[key].(string) + } + return params +} + +func decodeGetFrameParams(args [1]string, argsEscaped bool, r *http.Request) (params GetFrameParams, _ error) { + // Decode path: id. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "id", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + params.ID = c + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "id", + In: "path", + Err: err, + } + } + return params, nil +} diff --git a/api/rest/oas_request_decoders_gen.go b/api/rest/oas_request_decoders_gen.go new file mode 100644 index 0000000..2e0a01e --- /dev/null +++ b/api/rest/oas_request_decoders_gen.go @@ -0,0 +1,410 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "bytes" + "io" + "mime" + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/validate" +) + +func (s *Server) decodeListMetadataRequest(r *http.Request) ( + req *MetadataQuery, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request MetadataQuery + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeListMetadataEpochsRequest(r *http.Request) ( + req *MetadataQuery, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request MetadataQuery + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeListMetadataLabelsRequest(r *http.Request) ( + req *MetadataQuery, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request MetadataQuery + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeListMetadataNodesRequest(r *http.Request) ( + req *MetadataQuery, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request MetadataQuery + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + +func (s *Server) decodeListMetadataSlotsRequest(r *http.Request) ( + req *MetadataQuery, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request MetadataQuery + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} diff --git a/api/rest/oas_request_encoders_gen.go b/api/rest/oas_request_encoders_gen.go new file mode 100644 index 0000000..b849798 --- /dev/null +++ b/api/rest/oas_request_encoders_gen.go @@ -0,0 +1,81 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "bytes" + "net/http" + + "github.com/go-faster/jx" + ht "github.com/ogen-go/ogen/http" +) + +func encodeListMetadataRequest( + req *MetadataQuery, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + +func encodeListMetadataEpochsRequest( + req *MetadataQuery, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + +func encodeListMetadataLabelsRequest( + req *MetadataQuery, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + +func encodeListMetadataNodesRequest( + req *MetadataQuery, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + +func encodeListMetadataSlotsRequest( + req *MetadataQuery, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} diff --git a/api/rest/oas_response_decoders_gen.go b/api/rest/oas_response_decoders_gen.go new file mode 100644 index 0000000..bd891e6 --- /dev/null +++ b/api/rest/oas_response_decoders_gen.go @@ -0,0 +1,1303 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "io" + "mime" + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "github.com/ogen-go/ogen/conv" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/ogen-go/ogen/uri" + "github.com/ogen-go/ogen/validate" +) + +func decodeGetEthereumNowResponse(resp *http.Response) (res GetEthereumNowRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response GetEthereumNowOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper GetEthereumNowOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeGetEthereumSpecResponse(resp *http.Response) (res GetEthereumSpecRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response GetEthereumSpecOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + var wrapper GetEthereumSpecOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeGetFrameResponse(resp *http.Response) (res GetFrameRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response GetFrameOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper GetFrameOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 404: + // Code 404. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeListMetadataResponse(resp *http.Response) (res ListMetadataRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ListMetadataOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeListMetadataEpochsResponse(resp *http.Response) (res ListMetadataEpochsRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ListMetadataEpochsOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper ListMetadataEpochsOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeListMetadataLabelsResponse(resp *http.Response) (res ListMetadataLabelsRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ListMetadataLabelsOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper ListMetadataLabelsOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeListMetadataNodesResponse(resp *http.Response) (res ListMetadataNodesRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ListMetadataNodesOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper ListMetadataNodesOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + +func decodeListMetadataSlotsResponse(resp *http.Response) (res ListMetadataSlotsRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ListMetadataSlotsOK + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + var wrapper ListMetadataSlotsOKHeaders + wrapper.Response = response + h := uri.NewHeaderDecoder(resp.Header) + // Parse "Cache-Control" header. + { + cfg := uri.HeaderParameterDecodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := func() error { + if err := h.HasParam(cfg); err == nil { + if err := h.DecodeParam(cfg, func(d uri.Decoder) error { + var wrapperDotCacheControlVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + wrapperDotCacheControlVal = c + return nil + }(); err != nil { + return err + } + wrapper.CacheControl.SetTo(wrapperDotCacheControlVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "parse Cache-Control header") + } + } + return &wrapper, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Convenient error response. + defRes, err := func() (res *UnexpectedErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &UnexpectedErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} diff --git a/api/rest/oas_response_encoders_gen.go b/api/rest/oas_response_encoders_gen.go new file mode 100644 index 0000000..9bc67b5 --- /dev/null +++ b/api/rest/oas_response_encoders_gen.go @@ -0,0 +1,439 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "net/http" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + "github.com/ogen-go/ogen/conv" + ht "github.com/ogen-go/ogen/http" + "github.com/ogen-go/ogen/uri" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +func encodeGetEthereumNowResponse(response GetEthereumNowRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *GetEthereumNowOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeGetEthereumSpecResponse(response GetEthereumSpecRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *GetEthereumSpecOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeGetFrameResponse(response GetFrameRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *GetFrameOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(404) + span.SetStatus(codes.Error, http.StatusText(404)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeListMetadataResponse(response ListMetadataRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *ListMetadataOK: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeListMetadataEpochsResponse(response ListMetadataEpochsRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *ListMetadataEpochsOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeListMetadataLabelsResponse(response ListMetadataLabelsRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *ListMetadataLabelsOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeListMetadataNodesResponse(response ListMetadataNodesRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *ListMetadataNodesOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeListMetadataSlotsResponse(response ListMetadataSlotsRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *ListMetadataSlotsOKHeaders: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + // Encoding response headers. + { + h := uri.NewHeaderEncoder(w.Header()) + // Encode "Cache-Control" header. + { + cfg := uri.HeaderParameterEncodingConfig{ + Name: "Cache-Control", + Explode: false, + } + if err := h.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := response.CacheControl.Get(); ok { + return e.EncodeValue(conv.StringToString(val)) + } + return nil + }); err != nil { + return errors.Wrap(err, "encode Cache-Control header") + } + } + } + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *Error: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + +func encodeErrorResponse(response *UnexpectedErrorStatusCode, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + code := response.StatusCode + if code == 0 { + // Set default status code. + code = http.StatusOK + } + w.WriteHeader(code) + if st := http.StatusText(code); code >= http.StatusBadRequest { + span.SetStatus(codes.Error, st) + } else { + span.SetStatus(codes.Ok, st) + } + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + if code >= http.StatusInternalServerError { + return errors.Wrapf(ht.ErrInternalServerErrorResponse, "code: %d, message: %s", code, http.StatusText(code)) + } + return nil + +} diff --git a/api/rest/oas_router_gen.go b/api/rest/oas_router_gen.go new file mode 100644 index 0000000..68d41bc --- /dev/null +++ b/api/rest/oas_router_gen.go @@ -0,0 +1,665 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "net/http" + "net/url" + "strings" + + "github.com/ogen-go/ogen/uri" +) + +var ( + rn7AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn8AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn10AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn11AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } + rn12AllowedHeaders = map[string]string{ + "POST": "Content-Type", + } +) + +func (s *Server) cutPrefix(path string) (string, bool) { + prefix := s.cfg.Prefix + if prefix == "" { + return path, true + } + if !strings.HasPrefix(path, prefix) { + // Prefix doesn't match. + return "", false + } + // Cut prefix from the path. + return strings.TrimPrefix(path, prefix), true +} + +// ServeHTTP serves http request as defined by OpenAPI v3 specification, +// calling handler that matches the path or returning not found error. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + elem := r.URL.Path + elemIsEscaped := false + if rawPath := r.URL.RawPath; rawPath != "" { + if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { + elem = normalized + elemIsEscaped = strings.ContainsRune(elem, '%') + } + } + + elem, ok := s.cutPrefix(elem) + if !ok || len(elem) == 0 { + s.notFound(w, r) + return + } + args := [1]string{} + + // Static code generated router with unwrapped path search. + switch { + default: + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/api/v1/" + + if l := len("/api/v1/"); len(elem) >= l && elem[0:l] == "/api/v1/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'e': // Prefix: "ethereum/" + + if l := len("ethereum/"); len(elem) >= l && elem[0:l] == "ethereum/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'n': // Prefix: "now" + + if l := len("now"); len(elem) >= l && elem[0:l] == "now" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetEthereumNowRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "GET", + allowedHeaders: nil, + acceptPost: "", + acceptPatch: "", + }) + } + + return + } + + case 's': // Prefix: "spec" + + if l := len("spec"); len(elem) >= l && elem[0:l] == "spec" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetEthereumSpecRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "GET", + allowedHeaders: nil, + acceptPost: "", + acceptPatch: "", + }) + } + + return + } + + } + + case 'f': // Prefix: "frames/" + + if l := len("frames/"); len(elem) >= l && elem[0:l] == "frames/" { + elem = elem[l:] + } else { + break + } + + // Param: "id" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleGetFrameRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "GET", + allowedHeaders: nil, + acceptPost: "", + acceptPatch: "", + }) + } + + return + } + + case 'm': // Prefix: "metadata" + + if l := len("metadata"); len(elem) >= l && elem[0:l] == "metadata" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch r.Method { + case "POST": + s.handleListMetadataRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn7AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'e': // Prefix: "epochs" + + if l := len("epochs"); len(elem) >= l && elem[0:l] == "epochs" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleListMetadataEpochsRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn8AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + case 'l': // Prefix: "labels" + + if l := len("labels"); len(elem) >= l && elem[0:l] == "labels" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleListMetadataLabelsRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn10AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + case 'n': // Prefix: "nodes" + + if l := len("nodes"); len(elem) >= l && elem[0:l] == "nodes" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleListMetadataNodesRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn11AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + case 's': // Prefix: "slots" + + if l := len("slots"); len(elem) >= l && elem[0:l] == "slots" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleListMetadataSlotsRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn12AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + } + + } + + } + + } + } + s.notFound(w, r) +} + +// Route is route object. +type Route struct { + name string + summary string + operationID string + operationGroup string + pathPattern string + count int + args [1]string +} + +// Name returns ogen operation name. +// +// It is guaranteed to be unique and not empty. +func (r Route) Name() string { + return r.name +} + +// Summary returns OpenAPI summary. +func (r Route) Summary() string { + return r.summary +} + +// OperationID returns OpenAPI operationId. +func (r Route) OperationID() string { + return r.operationID +} + +// OperationGroup returns the x-ogen-operation-group value. +func (r Route) OperationGroup() string { + return r.operationGroup +} + +// PathPattern returns OpenAPI path. +func (r Route) PathPattern() string { + return r.pathPattern +} + +// Args returns parsed arguments. +func (r Route) Args() []string { + return r.args[:r.count] +} + +// FindRoute finds Route for given method and path. +// +// Note: this method does not unescape path or handle reserved characters in path properly. Use FindPath instead. +func (s *Server) FindRoute(method, path string) (Route, bool) { + return s.FindPath(method, &url.URL{Path: path}) +} + +// FindPath finds Route for given method and URL. +func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { + var ( + elem = u.Path + args = r.args + ) + if rawPath := u.RawPath; rawPath != "" { + if normalized, ok := uri.NormalizeEscapedPath(rawPath); ok { + elem = normalized + } + defer func() { + for i, arg := range r.args[:r.count] { + if unescaped, err := url.PathUnescape(arg); err == nil { + r.args[i] = unescaped + } + } + }() + } + + elem, ok := s.cutPrefix(elem) + if !ok { + return r, false + } + + // Static code generated router with unwrapped path search. + switch { + default: + if len(elem) == 0 { + break + } + switch elem[0] { + case '/': // Prefix: "/api/v1/" + + if l := len("/api/v1/"); len(elem) >= l && elem[0:l] == "/api/v1/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'e': // Prefix: "ethereum/" + + if l := len("ethereum/"); len(elem) >= l && elem[0:l] == "ethereum/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'n': // Prefix: "now" + + if l := len("now"); len(elem) >= l && elem[0:l] == "now" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = GetEthereumNowOperation + r.summary = "Get the current wall-clock slot and epoch" + r.operationID = "getEthereumNow" + r.operationGroup = "" + r.pathPattern = "/api/v1/ethereum/now" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + case 's': // Prefix: "spec" + + if l := len("spec"); len(elem) >= l && elem[0:l] == "spec" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = GetEthereumSpecOperation + r.summary = "Get the Ethereum network name and spec" + r.operationID = "getEthereumSpec" + r.operationGroup = "" + r.pathPattern = "/api/v1/ethereum/spec" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + } + + case 'f': // Prefix: "frames/" + + if l := len("frames/"); len(elem) >= l && elem[0:l] == "frames/" { + elem = elem[l:] + } else { + break + } + + // Param: "id" + // Leaf parameter, slashes are prohibited + idx := strings.IndexByte(elem, '/') + if idx >= 0 { + break + } + args[0] = elem + elem = "" + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = GetFrameOperation + r.summary = "Get a single fork-choice frame by ID" + r.operationID = "getFrame" + r.operationGroup = "" + r.pathPattern = "/api/v1/frames/{id}" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + case 'm': // Prefix: "metadata" + + if l := len("metadata"); len(elem) >= l && elem[0:l] == "metadata" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "POST": + r.name = ListMetadataOperation + r.summary = "List fork-choice frame metadata" + r.operationID = "listMetadata" + r.operationGroup = "" + r.pathPattern = "/api/v1/metadata" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + switch elem[0] { + case '/': // Prefix: "/" + + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + break + } + switch elem[0] { + case 'e': // Prefix: "epochs" + + if l := len("epochs"); len(elem) >= l && elem[0:l] == "epochs" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ListMetadataEpochsOperation + r.summary = "List distinct epochs across stored frames" + r.operationID = "listMetadataEpochs" + r.operationGroup = "" + r.pathPattern = "/api/v1/metadata/epochs" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + case 'l': // Prefix: "labels" + + if l := len("labels"); len(elem) >= l && elem[0:l] == "labels" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ListMetadataLabelsOperation + r.summary = "List distinct labels across stored frames" + r.operationID = "listMetadataLabels" + r.operationGroup = "" + r.pathPattern = "/api/v1/metadata/labels" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + case 'n': // Prefix: "nodes" + + if l := len("nodes"); len(elem) >= l && elem[0:l] == "nodes" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ListMetadataNodesOperation + r.summary = "List distinct nodes across stored frames" + r.operationID = "listMetadataNodes" + r.operationGroup = "" + r.pathPattern = "/api/v1/metadata/nodes" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + case 's': // Prefix: "slots" + + if l := len("slots"); len(elem) >= l && elem[0:l] == "slots" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = ListMetadataSlotsOperation + r.summary = "List distinct slots across stored frames" + r.operationID = "listMetadataSlots" + r.operationGroup = "" + r.pathPattern = "/api/v1/metadata/slots" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + } + + } + + } + + } + } + return r, false +} diff --git a/api/rest/oas_schemas_gen.go b/api/rest/oas_schemas_gen.go new file mode 100644 index 0000000..42f9ccd --- /dev/null +++ b/api/rest/oas_schemas_gen.go @@ -0,0 +1,1516 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "fmt" + "time" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" +) + +func (s *UnexpectedErrorStatusCode) Error() string { + return fmt.Sprintf("code %d: %+v", s.StatusCode, s.Response) +} + +// Ref: #/components/schemas/Checkpoint +type Checkpoint struct { + Epoch EpochString `json:"epoch"` + Root Root `json:"root"` +} + +// GetEpoch returns the value of Epoch. +func (s *Checkpoint) GetEpoch() EpochString { + return s.Epoch +} + +// GetRoot returns the value of Root. +func (s *Checkpoint) GetRoot() Root { + return s.Root +} + +// SetEpoch sets the value of Epoch. +func (s *Checkpoint) SetEpoch(val EpochString) { + s.Epoch = val +} + +// SetRoot sets the value of Root. +func (s *Checkpoint) SetRoot(val Root) { + s.Root = val +} + +type Epoch int64 + +type EpochString string + +// Ref: #/components/schemas/Error +type Error struct { + // The HTTP status code. + Code int `json:"code"` + // A human-readable error message. + Message string `json:"message"` +} + +// GetCode returns the value of Code. +func (s *Error) GetCode() int { + return s.Code +} + +// GetMessage returns the value of Message. +func (s *Error) GetMessage() string { + return s.Message +} + +// SetCode sets the value of Code. +func (s *Error) SetCode(val int) { + s.Code = val +} + +// SetMessage sets the value of Message. +func (s *Error) SetMessage(val string) { + s.Message = val +} + +func (*Error) getEthereumNowRes() {} +func (*Error) getEthereumSpecRes() {} +func (*Error) getFrameRes() {} +func (*Error) listMetadataEpochsRes() {} +func (*Error) listMetadataLabelsRes() {} +func (*Error) listMetadataNodesRes() {} +func (*Error) listMetadataRes() {} +func (*Error) listMetadataSlotsRes() {} + +// Ref: #/components/schemas/EthereumNow +type EthereumNow struct { + Slot Slot `json:"slot"` + Epoch Epoch `json:"epoch"` +} + +// GetSlot returns the value of Slot. +func (s *EthereumNow) GetSlot() Slot { + return s.Slot +} + +// GetEpoch returns the value of Epoch. +func (s *EthereumNow) GetEpoch() Epoch { + return s.Epoch +} + +// SetSlot sets the value of Slot. +func (s *EthereumNow) SetSlot(val Slot) { + s.Slot = val +} + +// SetEpoch sets the value of Epoch. +func (s *EthereumNow) SetEpoch(val Epoch) { + s.Epoch = val +} + +// Ref: #/components/schemas/EthereumSpec +type EthereumSpec struct { + SecondsPerSlot int64 `json:"seconds_per_slot"` + SlotsPerEpoch int64 `json:"slots_per_epoch"` + GenesisTime time.Time `json:"genesis_time"` +} + +// GetSecondsPerSlot returns the value of SecondsPerSlot. +func (s *EthereumSpec) GetSecondsPerSlot() int64 { + return s.SecondsPerSlot +} + +// GetSlotsPerEpoch returns the value of SlotsPerEpoch. +func (s *EthereumSpec) GetSlotsPerEpoch() int64 { + return s.SlotsPerEpoch +} + +// GetGenesisTime returns the value of GenesisTime. +func (s *EthereumSpec) GetGenesisTime() time.Time { + return s.GenesisTime +} + +// SetSecondsPerSlot sets the value of SecondsPerSlot. +func (s *EthereumSpec) SetSecondsPerSlot(val int64) { + s.SecondsPerSlot = val +} + +// SetSlotsPerEpoch sets the value of SlotsPerEpoch. +func (s *EthereumSpec) SetSlotsPerEpoch(val int64) { + s.SlotsPerEpoch = val +} + +// SetGenesisTime sets the value of GenesisTime. +func (s *EthereumSpec) SetGenesisTime(val time.Time) { + s.GenesisTime = val +} + +// Ref: #/components/schemas/EthereumSpecResult +type EthereumSpecResult struct { + NetworkName string `json:"network_name"` + Spec EthereumSpec `json:"spec"` +} + +// GetNetworkName returns the value of NetworkName. +func (s *EthereumSpecResult) GetNetworkName() string { + return s.NetworkName +} + +// GetSpec returns the value of Spec. +func (s *EthereumSpecResult) GetSpec() EthereumSpec { + return s.Spec +} + +// SetNetworkName sets the value of NetworkName. +func (s *EthereumSpecResult) SetNetworkName(val string) { + s.NetworkName = val +} + +// SetSpec sets the value of Spec. +func (s *EthereumSpecResult) SetSpec(val EthereumSpec) { + s.Spec = val +} + +// A beacon node's fork-choice dump. +// Ref: #/components/schemas/ForkChoice +type ForkChoice struct { + JustifiedCheckpoint Checkpoint `json:"justified_checkpoint"` + FinalizedCheckpoint Checkpoint `json:"finalized_checkpoint"` + ForkChoiceNodes []ForkChoiceNode `json:"fork_choice_nodes"` +} + +// GetJustifiedCheckpoint returns the value of JustifiedCheckpoint. +func (s *ForkChoice) GetJustifiedCheckpoint() Checkpoint { + return s.JustifiedCheckpoint +} + +// GetFinalizedCheckpoint returns the value of FinalizedCheckpoint. +func (s *ForkChoice) GetFinalizedCheckpoint() Checkpoint { + return s.FinalizedCheckpoint +} + +// GetForkChoiceNodes returns the value of ForkChoiceNodes. +func (s *ForkChoice) GetForkChoiceNodes() []ForkChoiceNode { + return s.ForkChoiceNodes +} + +// SetJustifiedCheckpoint sets the value of JustifiedCheckpoint. +func (s *ForkChoice) SetJustifiedCheckpoint(val Checkpoint) { + s.JustifiedCheckpoint = val +} + +// SetFinalizedCheckpoint sets the value of FinalizedCheckpoint. +func (s *ForkChoice) SetFinalizedCheckpoint(val Checkpoint) { + s.FinalizedCheckpoint = val +} + +// SetForkChoiceNodes sets the value of ForkChoiceNodes. +func (s *ForkChoice) SetForkChoiceNodes(val []ForkChoiceNode) { + s.ForkChoiceNodes = val +} + +// Ref: #/components/schemas/ForkChoiceNode +type ForkChoiceNode struct { + Slot SlotString `json:"slot"` + BlockRoot Root `json:"block_root"` + ParentRoot Root `json:"parent_root"` + JustifiedEpoch EpochString `json:"justified_epoch"` + FinalizedEpoch EpochString `json:"finalized_epoch"` + // The node's fork-choice weight, serialized as a string. + Weight string `json:"weight"` + // The node's validity status. + Validity ForkChoiceNodeValidity `json:"validity"` + ExecutionBlockHash Root `json:"execution_block_hash"` + ExtraData OptForkChoiceNodeExtraData `json:"extra_data"` +} + +// GetSlot returns the value of Slot. +func (s *ForkChoiceNode) GetSlot() SlotString { + return s.Slot +} + +// GetBlockRoot returns the value of BlockRoot. +func (s *ForkChoiceNode) GetBlockRoot() Root { + return s.BlockRoot +} + +// GetParentRoot returns the value of ParentRoot. +func (s *ForkChoiceNode) GetParentRoot() Root { + return s.ParentRoot +} + +// GetJustifiedEpoch returns the value of JustifiedEpoch. +func (s *ForkChoiceNode) GetJustifiedEpoch() EpochString { + return s.JustifiedEpoch +} + +// GetFinalizedEpoch returns the value of FinalizedEpoch. +func (s *ForkChoiceNode) GetFinalizedEpoch() EpochString { + return s.FinalizedEpoch +} + +// GetWeight returns the value of Weight. +func (s *ForkChoiceNode) GetWeight() string { + return s.Weight +} + +// GetValidity returns the value of Validity. +func (s *ForkChoiceNode) GetValidity() ForkChoiceNodeValidity { + return s.Validity +} + +// GetExecutionBlockHash returns the value of ExecutionBlockHash. +func (s *ForkChoiceNode) GetExecutionBlockHash() Root { + return s.ExecutionBlockHash +} + +// GetExtraData returns the value of ExtraData. +func (s *ForkChoiceNode) GetExtraData() OptForkChoiceNodeExtraData { + return s.ExtraData +} + +// SetSlot sets the value of Slot. +func (s *ForkChoiceNode) SetSlot(val SlotString) { + s.Slot = val +} + +// SetBlockRoot sets the value of BlockRoot. +func (s *ForkChoiceNode) SetBlockRoot(val Root) { + s.BlockRoot = val +} + +// SetParentRoot sets the value of ParentRoot. +func (s *ForkChoiceNode) SetParentRoot(val Root) { + s.ParentRoot = val +} + +// SetJustifiedEpoch sets the value of JustifiedEpoch. +func (s *ForkChoiceNode) SetJustifiedEpoch(val EpochString) { + s.JustifiedEpoch = val +} + +// SetFinalizedEpoch sets the value of FinalizedEpoch. +func (s *ForkChoiceNode) SetFinalizedEpoch(val EpochString) { + s.FinalizedEpoch = val +} + +// SetWeight sets the value of Weight. +func (s *ForkChoiceNode) SetWeight(val string) { + s.Weight = val +} + +// SetValidity sets the value of Validity. +func (s *ForkChoiceNode) SetValidity(val ForkChoiceNodeValidity) { + s.Validity = val +} + +// SetExecutionBlockHash sets the value of ExecutionBlockHash. +func (s *ForkChoiceNode) SetExecutionBlockHash(val Root) { + s.ExecutionBlockHash = val +} + +// SetExtraData sets the value of ExtraData. +func (s *ForkChoiceNode) SetExtraData(val OptForkChoiceNodeExtraData) { + s.ExtraData = val +} + +type ForkChoiceNodeExtraData map[string]jx.Raw + +func (s *ForkChoiceNodeExtraData) init() ForkChoiceNodeExtraData { + m := *s + if m == nil { + m = map[string]jx.Raw{} + *s = m + } + return m +} + +// The node's validity status. +type ForkChoiceNodeValidity string + +const ( + ForkChoiceNodeValidityValid ForkChoiceNodeValidity = "valid" + ForkChoiceNodeValidityInvalid ForkChoiceNodeValidity = "invalid" + ForkChoiceNodeValidityOptimistic ForkChoiceNodeValidity = "optimistic" + ForkChoiceNodeValidityUnknown ForkChoiceNodeValidity = "unknown" +) + +// AllValues returns all ForkChoiceNodeValidity values. +func (ForkChoiceNodeValidity) AllValues() []ForkChoiceNodeValidity { + return []ForkChoiceNodeValidity{ + ForkChoiceNodeValidityValid, + ForkChoiceNodeValidityInvalid, + ForkChoiceNodeValidityOptimistic, + ForkChoiceNodeValidityUnknown, + } +} + +// MarshalText implements encoding.TextMarshaler. +func (s ForkChoiceNodeValidity) MarshalText() ([]byte, error) { + switch s { + case ForkChoiceNodeValidityValid: + return []byte(s), nil + case ForkChoiceNodeValidityInvalid: + return []byte(s), nil + case ForkChoiceNodeValidityOptimistic: + return []byte(s), nil + case ForkChoiceNodeValidityUnknown: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *ForkChoiceNodeValidity) UnmarshalText(data []byte) error { + switch ForkChoiceNodeValidity(data) { + case ForkChoiceNodeValidityValid: + *s = ForkChoiceNodeValidityValid + return nil + case ForkChoiceNodeValidityInvalid: + *s = ForkChoiceNodeValidityInvalid + return nil + case ForkChoiceNodeValidityOptimistic: + *s = ForkChoiceNodeValidityOptimistic + return nil + case ForkChoiceNodeValidityUnknown: + *s = ForkChoiceNodeValidityUnknown + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + +// Ref: #/components/schemas/Frame +type Frame struct { + Data ForkChoice `json:"data"` + Metadata FrameMetadata `json:"metadata"` +} + +// GetData returns the value of Data. +func (s *Frame) GetData() ForkChoice { + return s.Data +} + +// GetMetadata returns the value of Metadata. +func (s *Frame) GetMetadata() FrameMetadata { + return s.Metadata +} + +// SetData sets the value of Data. +func (s *Frame) SetData(val ForkChoice) { + s.Data = val +} + +// SetMetadata sets the value of Metadata. +func (s *Frame) SetMetadata(val FrameMetadata) { + s.Metadata = val +} + +// Filters applied to a frame query. All fields are optional. +// Ref: #/components/schemas/FrameFilter +type FrameFilter struct { + Node OptString `json:"node"` + Before OptDateTime `json:"before"` + After OptDateTime `json:"after"` + Slot OptSlot `json:"slot"` + Epoch OptEpoch `json:"epoch"` + Labels []string `json:"labels"` + ConsensusClient OptString `json:"consensus_client"` + EventSource OptString `json:"event_source"` +} + +// GetNode returns the value of Node. +func (s *FrameFilter) GetNode() OptString { + return s.Node +} + +// GetBefore returns the value of Before. +func (s *FrameFilter) GetBefore() OptDateTime { + return s.Before +} + +// GetAfter returns the value of After. +func (s *FrameFilter) GetAfter() OptDateTime { + return s.After +} + +// GetSlot returns the value of Slot. +func (s *FrameFilter) GetSlot() OptSlot { + return s.Slot +} + +// GetEpoch returns the value of Epoch. +func (s *FrameFilter) GetEpoch() OptEpoch { + return s.Epoch +} + +// GetLabels returns the value of Labels. +func (s *FrameFilter) GetLabels() []string { + return s.Labels +} + +// GetConsensusClient returns the value of ConsensusClient. +func (s *FrameFilter) GetConsensusClient() OptString { + return s.ConsensusClient +} + +// GetEventSource returns the value of EventSource. +func (s *FrameFilter) GetEventSource() OptString { + return s.EventSource +} + +// SetNode sets the value of Node. +func (s *FrameFilter) SetNode(val OptString) { + s.Node = val +} + +// SetBefore sets the value of Before. +func (s *FrameFilter) SetBefore(val OptDateTime) { + s.Before = val +} + +// SetAfter sets the value of After. +func (s *FrameFilter) SetAfter(val OptDateTime) { + s.After = val +} + +// SetSlot sets the value of Slot. +func (s *FrameFilter) SetSlot(val OptSlot) { + s.Slot = val +} + +// SetEpoch sets the value of Epoch. +func (s *FrameFilter) SetEpoch(val OptEpoch) { + s.Epoch = val +} + +// SetLabels sets the value of Labels. +func (s *FrameFilter) SetLabels(val []string) { + s.Labels = val +} + +// SetConsensusClient sets the value of ConsensusClient. +func (s *FrameFilter) SetConsensusClient(val OptString) { + s.ConsensusClient = val +} + +// SetEventSource sets the value of EventSource. +func (s *FrameFilter) SetEventSource(val OptString) { + s.EventSource = val +} + +// Ref: #/components/schemas/FrameMetadata +type FrameMetadata struct { + // The frame ID. + ID string `json:"id"` + // The node that provided the frame. For a beacon node this is the + // configured node ID; for Xatu it is the Xatu Sentry ID. + Node string `json:"node"` + // When the frame was fetched. + FetchedAt time.Time `json:"fetched_at"` + WallClockSlot Slot `json:"wall_clock_slot"` + WallClockEpoch Epoch `json:"wall_clock_epoch"` + Labels []string `json:"labels"` + ConsensusClient string `json:"consensus_client"` + EventSource string `json:"event_source"` +} + +// GetID returns the value of ID. +func (s *FrameMetadata) GetID() string { + return s.ID +} + +// GetNode returns the value of Node. +func (s *FrameMetadata) GetNode() string { + return s.Node +} + +// GetFetchedAt returns the value of FetchedAt. +func (s *FrameMetadata) GetFetchedAt() time.Time { + return s.FetchedAt +} + +// GetWallClockSlot returns the value of WallClockSlot. +func (s *FrameMetadata) GetWallClockSlot() Slot { + return s.WallClockSlot +} + +// GetWallClockEpoch returns the value of WallClockEpoch. +func (s *FrameMetadata) GetWallClockEpoch() Epoch { + return s.WallClockEpoch +} + +// GetLabels returns the value of Labels. +func (s *FrameMetadata) GetLabels() []string { + return s.Labels +} + +// GetConsensusClient returns the value of ConsensusClient. +func (s *FrameMetadata) GetConsensusClient() string { + return s.ConsensusClient +} + +// GetEventSource returns the value of EventSource. +func (s *FrameMetadata) GetEventSource() string { + return s.EventSource +} + +// SetID sets the value of ID. +func (s *FrameMetadata) SetID(val string) { + s.ID = val +} + +// SetNode sets the value of Node. +func (s *FrameMetadata) SetNode(val string) { + s.Node = val +} + +// SetFetchedAt sets the value of FetchedAt. +func (s *FrameMetadata) SetFetchedAt(val time.Time) { + s.FetchedAt = val +} + +// SetWallClockSlot sets the value of WallClockSlot. +func (s *FrameMetadata) SetWallClockSlot(val Slot) { + s.WallClockSlot = val +} + +// SetWallClockEpoch sets the value of WallClockEpoch. +func (s *FrameMetadata) SetWallClockEpoch(val Epoch) { + s.WallClockEpoch = val +} + +// SetLabels sets the value of Labels. +func (s *FrameMetadata) SetLabels(val []string) { + s.Labels = val +} + +// SetConsensusClient sets the value of ConsensusClient. +func (s *FrameMetadata) SetConsensusClient(val string) { + s.ConsensusClient = val +} + +// SetEventSource sets the value of EventSource. +func (s *FrameMetadata) SetEventSource(val string) { + s.EventSource = val +} + +type GetEthereumNowOK struct { + Data EthereumNow `json:"data"` +} + +// GetData returns the value of Data. +func (s *GetEthereumNowOK) GetData() EthereumNow { + return s.Data +} + +// SetData sets the value of Data. +func (s *GetEthereumNowOK) SetData(val EthereumNow) { + s.Data = val +} + +// GetEthereumNowOKHeaders wraps GetEthereumNowOK with response headers. +type GetEthereumNowOKHeaders struct { + CacheControl OptString + Response GetEthereumNowOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *GetEthereumNowOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *GetEthereumNowOKHeaders) GetResponse() GetEthereumNowOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *GetEthereumNowOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *GetEthereumNowOKHeaders) SetResponse(val GetEthereumNowOK) { + s.Response = val +} + +func (*GetEthereumNowOKHeaders) getEthereumNowRes() {} + +type GetEthereumSpecOK struct { + Data EthereumSpecResult `json:"data"` +} + +// GetData returns the value of Data. +func (s *GetEthereumSpecOK) GetData() EthereumSpecResult { + return s.Data +} + +// SetData sets the value of Data. +func (s *GetEthereumSpecOK) SetData(val EthereumSpecResult) { + s.Data = val +} + +// GetEthereumSpecOKHeaders wraps GetEthereumSpecOK with response headers. +type GetEthereumSpecOKHeaders struct { + CacheControl OptString + Response GetEthereumSpecOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *GetEthereumSpecOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *GetEthereumSpecOKHeaders) GetResponse() GetEthereumSpecOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *GetEthereumSpecOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *GetEthereumSpecOKHeaders) SetResponse(val GetEthereumSpecOK) { + s.Response = val +} + +func (*GetEthereumSpecOKHeaders) getEthereumSpecRes() {} + +type GetFrameOK struct { + Data GetFrameOKData `json:"data"` +} + +// GetData returns the value of Data. +func (s *GetFrameOK) GetData() GetFrameOKData { + return s.Data +} + +// SetData sets the value of Data. +func (s *GetFrameOK) SetData(val GetFrameOKData) { + s.Data = val +} + +type GetFrameOKData struct { + Frame Frame `json:"frame"` +} + +// GetFrame returns the value of Frame. +func (s *GetFrameOKData) GetFrame() Frame { + return s.Frame +} + +// SetFrame sets the value of Frame. +func (s *GetFrameOKData) SetFrame(val Frame) { + s.Frame = val +} + +// GetFrameOKHeaders wraps GetFrameOK with response headers. +type GetFrameOKHeaders struct { + CacheControl OptString + Response GetFrameOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *GetFrameOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *GetFrameOKHeaders) GetResponse() GetFrameOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *GetFrameOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *GetFrameOKHeaders) SetResponse(val GetFrameOK) { + s.Response = val +} + +func (*GetFrameOKHeaders) getFrameRes() {} + +type ListMetadataEpochsOK struct { + Data ListMetadataEpochsOKData `json:"data"` +} + +// GetData returns the value of Data. +func (s *ListMetadataEpochsOK) GetData() ListMetadataEpochsOKData { + return s.Data +} + +// SetData sets the value of Data. +func (s *ListMetadataEpochsOK) SetData(val ListMetadataEpochsOKData) { + s.Data = val +} + +type ListMetadataEpochsOKData struct { + Epochs []Epoch `json:"epochs"` + Pagination PaginationResponse `json:"pagination"` +} + +// GetEpochs returns the value of Epochs. +func (s *ListMetadataEpochsOKData) GetEpochs() []Epoch { + return s.Epochs +} + +// GetPagination returns the value of Pagination. +func (s *ListMetadataEpochsOKData) GetPagination() PaginationResponse { + return s.Pagination +} + +// SetEpochs sets the value of Epochs. +func (s *ListMetadataEpochsOKData) SetEpochs(val []Epoch) { + s.Epochs = val +} + +// SetPagination sets the value of Pagination. +func (s *ListMetadataEpochsOKData) SetPagination(val PaginationResponse) { + s.Pagination = val +} + +// ListMetadataEpochsOKHeaders wraps ListMetadataEpochsOK with response headers. +type ListMetadataEpochsOKHeaders struct { + CacheControl OptString + Response ListMetadataEpochsOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *ListMetadataEpochsOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *ListMetadataEpochsOKHeaders) GetResponse() ListMetadataEpochsOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *ListMetadataEpochsOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *ListMetadataEpochsOKHeaders) SetResponse(val ListMetadataEpochsOK) { + s.Response = val +} + +func (*ListMetadataEpochsOKHeaders) listMetadataEpochsRes() {} + +type ListMetadataLabelsOK struct { + Data ListMetadataLabelsOKData `json:"data"` +} + +// GetData returns the value of Data. +func (s *ListMetadataLabelsOK) GetData() ListMetadataLabelsOKData { + return s.Data +} + +// SetData sets the value of Data. +func (s *ListMetadataLabelsOK) SetData(val ListMetadataLabelsOKData) { + s.Data = val +} + +type ListMetadataLabelsOKData struct { + Labels []string `json:"labels"` + Pagination PaginationResponse `json:"pagination"` +} + +// GetLabels returns the value of Labels. +func (s *ListMetadataLabelsOKData) GetLabels() []string { + return s.Labels +} + +// GetPagination returns the value of Pagination. +func (s *ListMetadataLabelsOKData) GetPagination() PaginationResponse { + return s.Pagination +} + +// SetLabels sets the value of Labels. +func (s *ListMetadataLabelsOKData) SetLabels(val []string) { + s.Labels = val +} + +// SetPagination sets the value of Pagination. +func (s *ListMetadataLabelsOKData) SetPagination(val PaginationResponse) { + s.Pagination = val +} + +// ListMetadataLabelsOKHeaders wraps ListMetadataLabelsOK with response headers. +type ListMetadataLabelsOKHeaders struct { + CacheControl OptString + Response ListMetadataLabelsOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *ListMetadataLabelsOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *ListMetadataLabelsOKHeaders) GetResponse() ListMetadataLabelsOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *ListMetadataLabelsOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *ListMetadataLabelsOKHeaders) SetResponse(val ListMetadataLabelsOK) { + s.Response = val +} + +func (*ListMetadataLabelsOKHeaders) listMetadataLabelsRes() {} + +type ListMetadataNodesOK struct { + Data ListMetadataNodesOKData `json:"data"` +} + +// GetData returns the value of Data. +func (s *ListMetadataNodesOK) GetData() ListMetadataNodesOKData { + return s.Data +} + +// SetData sets the value of Data. +func (s *ListMetadataNodesOK) SetData(val ListMetadataNodesOKData) { + s.Data = val +} + +type ListMetadataNodesOKData struct { + Nodes []string `json:"nodes"` + Pagination PaginationResponse `json:"pagination"` +} + +// GetNodes returns the value of Nodes. +func (s *ListMetadataNodesOKData) GetNodes() []string { + return s.Nodes +} + +// GetPagination returns the value of Pagination. +func (s *ListMetadataNodesOKData) GetPagination() PaginationResponse { + return s.Pagination +} + +// SetNodes sets the value of Nodes. +func (s *ListMetadataNodesOKData) SetNodes(val []string) { + s.Nodes = val +} + +// SetPagination sets the value of Pagination. +func (s *ListMetadataNodesOKData) SetPagination(val PaginationResponse) { + s.Pagination = val +} + +// ListMetadataNodesOKHeaders wraps ListMetadataNodesOK with response headers. +type ListMetadataNodesOKHeaders struct { + CacheControl OptString + Response ListMetadataNodesOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *ListMetadataNodesOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *ListMetadataNodesOKHeaders) GetResponse() ListMetadataNodesOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *ListMetadataNodesOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *ListMetadataNodesOKHeaders) SetResponse(val ListMetadataNodesOK) { + s.Response = val +} + +func (*ListMetadataNodesOKHeaders) listMetadataNodesRes() {} + +type ListMetadataOK struct { + Data ListMetadataOKData `json:"data"` +} + +// GetData returns the value of Data. +func (s *ListMetadataOK) GetData() ListMetadataOKData { + return s.Data +} + +// SetData sets the value of Data. +func (s *ListMetadataOK) SetData(val ListMetadataOKData) { + s.Data = val +} + +func (*ListMetadataOK) listMetadataRes() {} + +type ListMetadataOKData struct { + Frames []FrameMetadata `json:"frames"` + Pagination PaginationResponse `json:"pagination"` +} + +// GetFrames returns the value of Frames. +func (s *ListMetadataOKData) GetFrames() []FrameMetadata { + return s.Frames +} + +// GetPagination returns the value of Pagination. +func (s *ListMetadataOKData) GetPagination() PaginationResponse { + return s.Pagination +} + +// SetFrames sets the value of Frames. +func (s *ListMetadataOKData) SetFrames(val []FrameMetadata) { + s.Frames = val +} + +// SetPagination sets the value of Pagination. +func (s *ListMetadataOKData) SetPagination(val PaginationResponse) { + s.Pagination = val +} + +type ListMetadataSlotsOK struct { + Data ListMetadataSlotsOKData `json:"data"` +} + +// GetData returns the value of Data. +func (s *ListMetadataSlotsOK) GetData() ListMetadataSlotsOKData { + return s.Data +} + +// SetData sets the value of Data. +func (s *ListMetadataSlotsOK) SetData(val ListMetadataSlotsOKData) { + s.Data = val +} + +type ListMetadataSlotsOKData struct { + Slots []Slot `json:"slots"` + Pagination PaginationResponse `json:"pagination"` +} + +// GetSlots returns the value of Slots. +func (s *ListMetadataSlotsOKData) GetSlots() []Slot { + return s.Slots +} + +// GetPagination returns the value of Pagination. +func (s *ListMetadataSlotsOKData) GetPagination() PaginationResponse { + return s.Pagination +} + +// SetSlots sets the value of Slots. +func (s *ListMetadataSlotsOKData) SetSlots(val []Slot) { + s.Slots = val +} + +// SetPagination sets the value of Pagination. +func (s *ListMetadataSlotsOKData) SetPagination(val PaginationResponse) { + s.Pagination = val +} + +// ListMetadataSlotsOKHeaders wraps ListMetadataSlotsOK with response headers. +type ListMetadataSlotsOKHeaders struct { + CacheControl OptString + Response ListMetadataSlotsOK +} + +// GetCacheControl returns the value of CacheControl. +func (s *ListMetadataSlotsOKHeaders) GetCacheControl() OptString { + return s.CacheControl +} + +// GetResponse returns the value of Response. +func (s *ListMetadataSlotsOKHeaders) GetResponse() ListMetadataSlotsOK { + return s.Response +} + +// SetCacheControl sets the value of CacheControl. +func (s *ListMetadataSlotsOKHeaders) SetCacheControl(val OptString) { + s.CacheControl = val +} + +// SetResponse sets the value of Response. +func (s *ListMetadataSlotsOKHeaders) SetResponse(val ListMetadataSlotsOK) { + s.Response = val +} + +func (*ListMetadataSlotsOKHeaders) listMetadataSlotsRes() {} + +// Ref: #/components/schemas/MetadataQuery +type MetadataQuery struct { + Filter OptFrameFilter `json:"filter"` + Pagination OptPaginationCursor `json:"pagination"` +} + +// GetFilter returns the value of Filter. +func (s *MetadataQuery) GetFilter() OptFrameFilter { + return s.Filter +} + +// GetPagination returns the value of Pagination. +func (s *MetadataQuery) GetPagination() OptPaginationCursor { + return s.Pagination +} + +// SetFilter sets the value of Filter. +func (s *MetadataQuery) SetFilter(val OptFrameFilter) { + s.Filter = val +} + +// SetPagination sets the value of Pagination. +func (s *MetadataQuery) SetPagination(val OptPaginationCursor) { + s.Pagination = val +} + +// NewOptDateTime returns new OptDateTime with value set to v. +func NewOptDateTime(v time.Time) OptDateTime { + return OptDateTime{ + Value: v, + Set: true, + } +} + +// OptDateTime is optional time.Time. +type OptDateTime struct { + Value time.Time + Set bool +} + +// IsSet returns true if OptDateTime was set. +func (o OptDateTime) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptDateTime) Reset() { + var v time.Time + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptDateTime) SetTo(v time.Time) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptDateTime) Get() (v time.Time, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptDateTime) Or(d time.Time) time.Time { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptEpoch returns new OptEpoch with value set to v. +func NewOptEpoch(v Epoch) OptEpoch { + return OptEpoch{ + Value: v, + Set: true, + } +} + +// OptEpoch is optional Epoch. +type OptEpoch struct { + Value Epoch + Set bool +} + +// IsSet returns true if OptEpoch was set. +func (o OptEpoch) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptEpoch) Reset() { + var v Epoch + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptEpoch) SetTo(v Epoch) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptEpoch) Get() (v Epoch, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptEpoch) Or(d Epoch) Epoch { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptForkChoiceNodeExtraData returns new OptForkChoiceNodeExtraData with value set to v. +func NewOptForkChoiceNodeExtraData(v ForkChoiceNodeExtraData) OptForkChoiceNodeExtraData { + return OptForkChoiceNodeExtraData{ + Value: v, + Set: true, + } +} + +// OptForkChoiceNodeExtraData is optional ForkChoiceNodeExtraData. +type OptForkChoiceNodeExtraData struct { + Value ForkChoiceNodeExtraData + Set bool +} + +// IsSet returns true if OptForkChoiceNodeExtraData was set. +func (o OptForkChoiceNodeExtraData) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptForkChoiceNodeExtraData) Reset() { + var v ForkChoiceNodeExtraData + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptForkChoiceNodeExtraData) SetTo(v ForkChoiceNodeExtraData) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptForkChoiceNodeExtraData) Get() (v ForkChoiceNodeExtraData, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptForkChoiceNodeExtraData) Or(d ForkChoiceNodeExtraData) ForkChoiceNodeExtraData { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptFrameFilter returns new OptFrameFilter with value set to v. +func NewOptFrameFilter(v FrameFilter) OptFrameFilter { + return OptFrameFilter{ + Value: v, + Set: true, + } +} + +// OptFrameFilter is optional FrameFilter. +type OptFrameFilter struct { + Value FrameFilter + Set bool +} + +// IsSet returns true if OptFrameFilter was set. +func (o OptFrameFilter) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptFrameFilter) Reset() { + var v FrameFilter + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptFrameFilter) SetTo(v FrameFilter) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptFrameFilter) Get() (v FrameFilter, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptFrameFilter) Or(d FrameFilter) FrameFilter { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptInt returns new OptInt with value set to v. +func NewOptInt(v int) OptInt { + return OptInt{ + Value: v, + Set: true, + } +} + +// OptInt is optional int. +type OptInt struct { + Value int + Set bool +} + +// IsSet returns true if OptInt was set. +func (o OptInt) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptInt) Reset() { + var v int + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptInt) SetTo(v int) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptInt) Get() (v int, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptInt) Or(d int) int { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptPaginationCursor returns new OptPaginationCursor with value set to v. +func NewOptPaginationCursor(v PaginationCursor) OptPaginationCursor { + return OptPaginationCursor{ + Value: v, + Set: true, + } +} + +// OptPaginationCursor is optional PaginationCursor. +type OptPaginationCursor struct { + Value PaginationCursor + Set bool +} + +// IsSet returns true if OptPaginationCursor was set. +func (o OptPaginationCursor) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptPaginationCursor) Reset() { + var v PaginationCursor + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptPaginationCursor) SetTo(v PaginationCursor) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptPaginationCursor) Get() (v PaginationCursor, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptPaginationCursor) Or(d PaginationCursor) PaginationCursor { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptSlot returns new OptSlot with value set to v. +func NewOptSlot(v Slot) OptSlot { + return OptSlot{ + Value: v, + Set: true, + } +} + +// OptSlot is optional Slot. +type OptSlot struct { + Value Slot + Set bool +} + +// IsSet returns true if OptSlot was set. +func (o OptSlot) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptSlot) Reset() { + var v Slot + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptSlot) SetTo(v Slot) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptSlot) Get() (v Slot, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptSlot) Or(d Slot) Slot { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// NewOptString returns new OptString with value set to v. +func NewOptString(v string) OptString { + return OptString{ + Value: v, + Set: true, + } +} + +// OptString is optional string. +type OptString struct { + Value string + Set bool +} + +// IsSet returns true if OptString was set. +func (o OptString) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptString) Reset() { + var v string + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptString) SetTo(v string) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptString) Get() (v string, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptString) Or(d string) string { + if v, ok := o.Get(); ok { + return v + } + return d +} + +// Offset-based pagination cursor. +// Ref: #/components/schemas/PaginationCursor +type PaginationCursor struct { + // The number of items to skip. + Offset OptInt `json:"offset"` + // The maximum number of items to return. + Limit OptInt `json:"limit"` +} + +// GetOffset returns the value of Offset. +func (s *PaginationCursor) GetOffset() OptInt { + return s.Offset +} + +// GetLimit returns the value of Limit. +func (s *PaginationCursor) GetLimit() OptInt { + return s.Limit +} + +// SetOffset sets the value of Offset. +func (s *PaginationCursor) SetOffset(val OptInt) { + s.Offset = val +} + +// SetLimit sets the value of Limit. +func (s *PaginationCursor) SetLimit(val OptInt) { + s.Limit = val +} + +// Ref: #/components/schemas/PaginationResponse +type PaginationResponse struct { + // The total number of matching items. + Total int64 `json:"total"` +} + +// GetTotal returns the value of Total. +func (s *PaginationResponse) GetTotal() int64 { + return s.Total +} + +// SetTotal sets the value of Total. +func (s *PaginationResponse) SetTotal(val int64) { + s.Total = val +} + +type Root string + +type Slot int64 + +type SlotString string + +// UnexpectedErrorStatusCode wraps Error with StatusCode. +type UnexpectedErrorStatusCode struct { + StatusCode int + Response Error +} + +// GetStatusCode returns the value of StatusCode. +func (s *UnexpectedErrorStatusCode) GetStatusCode() int { + return s.StatusCode +} + +// GetResponse returns the value of Response. +func (s *UnexpectedErrorStatusCode) GetResponse() Error { + return s.Response +} + +// SetStatusCode sets the value of StatusCode. +func (s *UnexpectedErrorStatusCode) SetStatusCode(val int) { + s.StatusCode = val +} + +// SetResponse sets the value of Response. +func (s *UnexpectedErrorStatusCode) SetResponse(val Error) { + s.Response = val +} diff --git a/api/rest/oas_server_gen.go b/api/rest/oas_server_gen.go new file mode 100644 index 0000000..4f9118c --- /dev/null +++ b/api/rest/oas_server_gen.go @@ -0,0 +1,82 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "context" +) + +// Handler handles operations described by OpenAPI v3 specification. +type Handler interface { + // GetEthereumNow implements getEthereumNow operation. + // + // Get the current wall-clock slot and epoch. + // + // GET /api/v1/ethereum/now + GetEthereumNow(ctx context.Context) (GetEthereumNowRes, error) + // GetEthereumSpec implements getEthereumSpec operation. + // + // Get the Ethereum network name and spec. + // + // GET /api/v1/ethereum/spec + GetEthereumSpec(ctx context.Context) (GetEthereumSpecRes, error) + // GetFrame implements getFrame operation. + // + // Get a single fork-choice frame by ID. + // + // GET /api/v1/frames/{id} + GetFrame(ctx context.Context, params GetFrameParams) (GetFrameRes, error) + // ListMetadata implements listMetadata operation. + // + // List fork-choice frame metadata. + // + // POST /api/v1/metadata + ListMetadata(ctx context.Context, req *MetadataQuery) (ListMetadataRes, error) + // ListMetadataEpochs implements listMetadataEpochs operation. + // + // List distinct epochs across stored frames. + // + // POST /api/v1/metadata/epochs + ListMetadataEpochs(ctx context.Context, req *MetadataQuery) (ListMetadataEpochsRes, error) + // ListMetadataLabels implements listMetadataLabels operation. + // + // List distinct labels across stored frames. + // + // POST /api/v1/metadata/labels + ListMetadataLabels(ctx context.Context, req *MetadataQuery) (ListMetadataLabelsRes, error) + // ListMetadataNodes implements listMetadataNodes operation. + // + // List distinct nodes across stored frames. + // + // POST /api/v1/metadata/nodes + ListMetadataNodes(ctx context.Context, req *MetadataQuery) (ListMetadataNodesRes, error) + // ListMetadataSlots implements listMetadataSlots operation. + // + // List distinct slots across stored frames. + // + // POST /api/v1/metadata/slots + ListMetadataSlots(ctx context.Context, req *MetadataQuery) (ListMetadataSlotsRes, error) + // NewError creates *UnexpectedErrorStatusCode from error returned by handler. + // + // Used for common default response. + NewError(ctx context.Context, err error) *UnexpectedErrorStatusCode +} + +// Server implements http server based on OpenAPI v3 specification and +// calls Handler to handle requests. +type Server struct { + h Handler + baseServer +} + +// NewServer creates new Server. +func NewServer(h Handler, opts ...ServerOption) (*Server, error) { + s, err := newServerConfig(opts...).baseServer() + if err != nil { + return nil, err + } + return &Server{ + h: h, + baseServer: s, + }, nil +} diff --git a/api/rest/oas_unimplemented_gen.go b/api/rest/oas_unimplemented_gen.go new file mode 100644 index 0000000..8af8d5c --- /dev/null +++ b/api/rest/oas_unimplemented_gen.go @@ -0,0 +1,94 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "context" + + ht "github.com/ogen-go/ogen/http" +) + +// UnimplementedHandler is no-op Handler which returns http.ErrNotImplemented. +type UnimplementedHandler struct{} + +var _ Handler = UnimplementedHandler{} + +// GetEthereumNow implements getEthereumNow operation. +// +// Get the current wall-clock slot and epoch. +// +// GET /api/v1/ethereum/now +func (UnimplementedHandler) GetEthereumNow(ctx context.Context) (r GetEthereumNowRes, _ error) { + return r, ht.ErrNotImplemented +} + +// GetEthereumSpec implements getEthereumSpec operation. +// +// Get the Ethereum network name and spec. +// +// GET /api/v1/ethereum/spec +func (UnimplementedHandler) GetEthereumSpec(ctx context.Context) (r GetEthereumSpecRes, _ error) { + return r, ht.ErrNotImplemented +} + +// GetFrame implements getFrame operation. +// +// Get a single fork-choice frame by ID. +// +// GET /api/v1/frames/{id} +func (UnimplementedHandler) GetFrame(ctx context.Context, params GetFrameParams) (r GetFrameRes, _ error) { + return r, ht.ErrNotImplemented +} + +// ListMetadata implements listMetadata operation. +// +// List fork-choice frame metadata. +// +// POST /api/v1/metadata +func (UnimplementedHandler) ListMetadata(ctx context.Context, req *MetadataQuery) (r ListMetadataRes, _ error) { + return r, ht.ErrNotImplemented +} + +// ListMetadataEpochs implements listMetadataEpochs operation. +// +// List distinct epochs across stored frames. +// +// POST /api/v1/metadata/epochs +func (UnimplementedHandler) ListMetadataEpochs(ctx context.Context, req *MetadataQuery) (r ListMetadataEpochsRes, _ error) { + return r, ht.ErrNotImplemented +} + +// ListMetadataLabels implements listMetadataLabels operation. +// +// List distinct labels across stored frames. +// +// POST /api/v1/metadata/labels +func (UnimplementedHandler) ListMetadataLabels(ctx context.Context, req *MetadataQuery) (r ListMetadataLabelsRes, _ error) { + return r, ht.ErrNotImplemented +} + +// ListMetadataNodes implements listMetadataNodes operation. +// +// List distinct nodes across stored frames. +// +// POST /api/v1/metadata/nodes +func (UnimplementedHandler) ListMetadataNodes(ctx context.Context, req *MetadataQuery) (r ListMetadataNodesRes, _ error) { + return r, ht.ErrNotImplemented +} + +// ListMetadataSlots implements listMetadataSlots operation. +// +// List distinct slots across stored frames. +// +// POST /api/v1/metadata/slots +func (UnimplementedHandler) ListMetadataSlots(ctx context.Context, req *MetadataQuery) (r ListMetadataSlotsRes, _ error) { + return r, ht.ErrNotImplemented +} + +// NewError creates *UnexpectedErrorStatusCode from error returned by handler. +// +// Used for common default response. +func (UnimplementedHandler) NewError(ctx context.Context, err error) (r *UnexpectedErrorStatusCode) { + r = new(UnexpectedErrorStatusCode) + return r +} diff --git a/api/rest/oas_validators_gen.go b/api/rest/oas_validators_gen.go new file mode 100644 index 0000000..9610601 --- /dev/null +++ b/api/rest/oas_validators_gen.go @@ -0,0 +1,803 @@ +// Code generated by ogen, DO NOT EDIT. + +package rest + +import ( + "fmt" + + "github.com/go-faster/errors" + "github.com/ogen-go/ogen/validate" +) + +func (s Epoch) Validate() error { + alias := (int64)(s) + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + Pattern: nil, + }).Validate(int64(alias)); err != nil { + return errors.Wrap(err, "int") + } + return nil +} + +func (s *EthereumNow) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Slot.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "slot", + Error: err, + }) + } + if err := func() error { + if err := s.Epoch.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "epoch", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ForkChoice) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.ForkChoiceNodes == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.ForkChoiceNodes { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "fork_choice_nodes", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ForkChoiceNode) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Validity.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "validity", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s ForkChoiceNodeValidity) Validate() error { + switch s { + case "valid": + return nil + case "invalid": + return nil + case "optimistic": + return nil + case "unknown": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + +func (s *Frame) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if err := func() error { + if err := s.Metadata.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "metadata", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *FrameFilter) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if value, ok := s.Slot.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "slot", + Error: err, + }) + } + if err := func() error { + if value, ok := s.Epoch.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "epoch", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *FrameMetadata) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.WallClockSlot.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "wall_clock_slot", + Error: err, + }) + } + if err := func() error { + if err := s.WallClockEpoch.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "wall_clock_epoch", + Error: err, + }) + } + if err := func() error { + if s.Labels == nil { + return errors.New("nil is invalid value") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "labels", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *GetEthereumNowOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *GetEthereumNowOKHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *GetFrameOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *GetFrameOKData) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Frame.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "frame", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *GetFrameOKHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataEpochsOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataEpochsOKData) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Epochs == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Epochs { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "epochs", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataEpochsOKHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataLabelsOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataLabelsOKData) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Labels == nil { + return errors.New("nil is invalid value") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "labels", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataLabelsOKHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataNodesOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataNodesOKData) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Nodes == nil { + return errors.New("nil is invalid value") + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "nodes", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataNodesOKHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataOKData) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Frames == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Frames { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "frames", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataSlotsOK) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Data.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "data", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataSlotsOKData) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Slots == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Slots { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "slots", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *ListMetadataSlotsOKHeaders) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "Response", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *MetadataQuery) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if value, ok := s.Filter.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "filter", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s Slot) Validate() error { + alias := (int64)(s) + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + Pattern: nil, + }).Validate(int64(alias)); err != nil { + return errors.Wrap(err, "int") + } + return nil +} diff --git a/api/spec.go b/api/spec.go new file mode 100644 index 0000000..87722c1 --- /dev/null +++ b/api/spec.go @@ -0,0 +1,8 @@ +package api + +import _ "embed" + +// OpenAPISpec contains the canonical Forky OpenAPI specification. +// +//go:embed openapi.yaml +var OpenAPISpec []byte diff --git a/cmd/forky/main.go b/cmd/forky/main.go new file mode 100644 index 0000000..47a52f1 --- /dev/null +++ b/cmd/forky/main.go @@ -0,0 +1,76 @@ +// Command forky fetches and serves Ethereum fork choice data. It wires up the +// CLI, loads configuration, and runs the application. +package main + +import ( + "context" + //nolint:gosec // pprof is served on a separate, operator-controlled port + _ "net/http/pprof" + "os" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + forky "github.com/ethpandaops/forky" + "github.com/ethpandaops/forky/internal/version" +) + +var ( + cfgFile string + log = logrus.New() +) + +func main() { + if err := rootCmd().Execute(); err != nil { + os.Exit(1) + } +} + +func rootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "forky", + Short: "Fetches and serves Ethereum fork choice data", + Run: func(_ *cobra.Command, _ []string) { + cfg := initCommon() + + server := forky.NewServer(log, cfg) + if err := server.Start(context.Background()); err != nil { + log.WithError(err).Fatal("failed to serve") + } + }, + } + + cmd.PersistentFlags().StringVar(&cfgFile, "config", "config.yaml", "config file (default is config.yaml)") + cmd.AddCommand(versionCmd()) + + return cmd +} + +func initCommon() *forky.Config { + log.SetFormatter(&logrus.TextFormatter{}) + log.WithField("file", cfgFile).Info("Loading config") + + config, err := forky.NewConfigFromYAMLFile(cfgFile) + if err != nil { + log.Fatal(err) + } + + logLevel, err := logrus.ParseLevel(config.LogLevel) + if err != nil { + log.WithField("log_level", config.LogLevel).Fatal("invalid log level") + } + + log.SetLevel(logLevel) + + return config +} + +func versionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print version information", + Run: func(cmd *cobra.Command, _ []string) { + cmd.Println(version.Full()) + }, + } +} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 5df3253..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "context" - "os" - - "github.com/ethpandaops/forky/pkg/forky" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "forky", - Short: "Fetches and serves Ethereum fork choice data", - Run: func(cmd *cobra.Command, args []string) { - cfg := initCommon() - p := forky.NewServer(log, cfg) - if err := p.Start(context.Background()); err != nil { - log.WithError(err).Fatal("failed to serve") - } - }, -} - -var ( - cfgFile string - log = logrus.New() -) - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.yaml", "config file (default is config.yaml)") -} - -func initCommon() *forky.Config { - log.SetFormatter(&logrus.TextFormatter{}) - - log.WithField("file", cfgFile).Info("Loading config") - - config, err := forky.NewConfigFromYAMLFile(cfgFile) - if err != nil { - log.Fatal(err) - } - - logLevel, err := logrus.ParseLevel(config.LogLevel) - if err != nil { - log.WithField("log_level", config.LogLevel).Fatal("invalid log level") - } - - log.SetLevel(logLevel) - - return config -} diff --git a/pkg/forky/config.go b/config.go similarity index 91% rename from pkg/forky/config.go rename to config.go index 564c008..d66340b 100644 --- a/pkg/forky/config.go +++ b/config.go @@ -4,9 +4,10 @@ import ( "os" "github.com/creasty/defaults" - "github.com/ethpandaops/forky/pkg/forky/api" - "github.com/ethpandaops/forky/pkg/forky/service" "gopkg.in/yaml.v2" + + "github.com/ethpandaops/forky/internal/api" + "github.com/ethpandaops/forky/internal/service" ) type Config struct { diff --git a/pkg/forky/forkchoice.go b/forkchoice.go similarity index 76% rename from pkg/forky/forkchoice.go rename to forkchoice.go index dd6bea0..1d3bc1f 100644 --- a/pkg/forky/forkchoice.go +++ b/forkchoice.go @@ -2,16 +2,16 @@ package forky import ( "context" - "io/fs" "net/http" "time" - "github.com/ethpandaops/forky/pkg/forky/api" - "github.com/ethpandaops/forky/pkg/forky/service" - static "github.com/ethpandaops/forky/web" - "github.com/julienschmidt/httprouter" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" + + openapi "github.com/ethpandaops/forky/api" + "github.com/ethpandaops/forky/internal/api" + "github.com/ethpandaops/forky/internal/service" + "github.com/ethpandaops/forky/web" ) // Server is the main server for the forkchoice service. @@ -66,34 +66,42 @@ func (s *Server) Start(ctx context.Context) error { } } - router := httprouter.New() - - frontend, err := fs.Sub(static.FS, "build/frontend") + frontend, err := web.GetFS() if err != nil { return err } filesystem := http.FS(frontend) - router.NotFound = wrapHandler(http.FileServer(filesystem), filesystem) - if s.Cfg.Metrics.Enabled { - if err := s.ServeMetrics(ctx); err != nil { - return err + if metricsErr := s.ServeMetrics(ctx); metricsErr != nil { + return metricsErr } } + apiHandler, err := s.http.Handler() + if err != nil { + return err + } + + mux := http.NewServeMux() + mux.Handle("/api/", apiHandler) + mux.HandleFunc("GET /openapi.yaml", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/yaml") + + if _, err := w.Write(openapi.OpenAPISpec); err != nil { + s.log.WithError(err).Error("Failed to write OpenAPI spec") + } + }) + mux.Handle("/", wrapHandler(http.FileServer(filesystem), filesystem)) + server := &http.Server{ Addr: s.Cfg.ListenAddr, ReadHeaderTimeout: 3 * time.Minute, WriteTimeout: 15 * time.Minute, } - if err := s.http.BindToRouter(ctx, router); err != nil { - return err - } - - server.Handler = router + server.Handler = mux s.log.Infof("Serving http at %s", s.Cfg.ListenAddr) diff --git a/pkg/forky/forky_test.go b/forky_test.go similarity index 84% rename from pkg/forky/forky_test.go rename to forky_test.go index ae35633..9c07a21 100644 --- a/pkg/forky/forky_test.go +++ b/forky_test.go @@ -7,16 +7,16 @@ import ( "testing" "time" - "github.com/ethpandaops/forky/pkg/forky/service" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" -) + "github.com/stretchr/testify/require" -var ( - testDBCounter = 0 + "github.com/ethpandaops/forky/internal/service" + "github.com/ethpandaops/forky/internal/types" ) +var testDBCounter = 0 + func newTestServer(withConfig string) (Server, error) { // create mock log object log := logrus.New() @@ -72,7 +72,7 @@ forky: func TestForkChoiceServer(t *testing.T) { t.Run("Add a random frame", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) go func() { err = s.Start(context.Background()) @@ -83,12 +83,12 @@ func TestForkChoiceServer(t *testing.T) { frame := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", frame) - assert.NoError(t, err) + require.NoError(t, err) }) t.Run("Add and get a frame", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) go func() { err = s.Start(context.Background()) @@ -99,17 +99,17 @@ func TestForkChoiceServer(t *testing.T) { frame := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", frame) - assert.NoError(t, err) + require.NoError(t, err) // query the frame f, err := s.svc.GetFrame(context.Background(), frame.Metadata.ID) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, frame.Metadata.ID, f.Metadata.ID) }) t.Run("Add and list a frame", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) go func() { err = s.Start(context.Background()) @@ -120,19 +120,19 @@ func TestForkChoiceServer(t *testing.T) { frame := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", frame) - assert.NoError(t, err) + require.NoError(t, err) // query the frame f, _, err := s.svc.ListMetadata(context.Background(), &service.FrameFilter{ Node: &frame.Metadata.Node, }, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, frame.Metadata.ID, f[0].ID) }) t.Run("Add and list a frame by its metadata types", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) go func() { err = s.Start(context.Background()) @@ -143,37 +143,37 @@ func TestForkChoiceServer(t *testing.T) { frame := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", frame) - assert.NoError(t, err) + require.NoError(t, err) // query the frame f, _, err := s.svc.ListNodes(context.Background(), &service.FrameFilter{ Node: &frame.Metadata.Node, }, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, frame.Metadata.Node, f[0]) f, _, err = s.svc.ListLabels(context.Background(), &service.FrameFilter{ Node: &frame.Metadata.Node, }, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, frame.Metadata.Labels[0], f[0]) epochs, _, err := s.svc.ListEpochs(context.Background(), &service.FrameFilter{ Node: &frame.Metadata.Node, }, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, frame.Metadata.WallClockEpoch, epochs[0]) slots, _, err := s.svc.ListSlots(context.Background(), &service.FrameFilter{ Node: &frame.Metadata.Node, }, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, frame.Metadata.WallClockSlot, slots[0]) }) t.Run("Pagination", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) go func() { err = s.Start(context.Background()) @@ -184,21 +184,21 @@ func TestForkChoiceServer(t *testing.T) { //nolint:gosec // This is a test function. framesToCreate := rand.Intn(500) - for i := 0; i < framesToCreate; i++ { + for range framesToCreate { f := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", f) - assert.NoError(t, err) + require.NoError(t, err) } // query the frame _, page, err := s.svc.ListMetadata(context.Background(), &service.FrameFilter{}, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, page.Total, int64(framesToCreate)) }) t.Run("Retention period purging", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) s.Cfg.Forky.RetentionPeriod.Duration = 1 * time.Second @@ -211,26 +211,26 @@ func TestForkChoiceServer(t *testing.T) { //nolint:gosec // This is a test function. framesToCreate := rand.Intn(500) - for i := 0; i < framesToCreate; i++ { + for range framesToCreate { f := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", f) - assert.NoError(t, err) + require.NoError(t, err) } time.Sleep(5 * time.Second) err = s.svc.DeleteOldFrames(context.Background()) - assert.NoError(t, err) + require.NoError(t, err) // query the frame _, page, err := s.svc.ListMetadata(context.Background(), &service.FrameFilter{}, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, int64(0), page.Total) }) t.Run("Add and delete frames", func(t *testing.T) { s, err := newTestServer("") - assert.NoError(t, err) + require.NoError(t, err) go func() { err = s.Start(context.Background()) @@ -241,26 +241,26 @@ func TestForkChoiceServer(t *testing.T) { //nolint:gosec // This is a test function. framesToCreate := rand.Intn(500) - for i := 0; i < framesToCreate; i++ { + for range framesToCreate { f := types.GenerateFakeFrame() err = s.svc.AddNewFrame(context.Background(), "fake", f) - assert.NoError(t, err) + require.NoError(t, err) } frames, page, err := s.svc.ListMetadata(context.Background(), &service.FrameFilter{}, *service.DefaultPagination()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, page.Total, int64(framesToCreate)) assert.Equal(t, len(frames), framesToCreate) for _, f := range frames { err = s.svc.DeleteFrame(context.Background(), f.ID) - assert.NoError(t, err) + require.NoError(t, err) } // query the frame newFrames, page, err := s.svc.ListMetadata(context.Background(), &service.FrameFilter{}, *service.DefaultPagination()) - assert.NoError(t, err) - assert.Equal(t, page.Total, int64(0)) - assert.Equal(t, len(newFrames), 0) + require.NoError(t, err) + assert.Equal(t, int64(0), page.Total) + assert.Empty(t, newFrames) }) } diff --git a/go.mod b/go.mod index 58d6892..a58606e 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/ethpandaops/forky -go 1.24.0 - -toolchain go1.24.5 +go 1.26.2 replace github.com/probe-lab/hermes => github.com/ethpandaops/hermes v0.0.4-0.20250715094801-db6021d1c703 @@ -20,16 +18,19 @@ require ( github.com/ethpandaops/xatu v1.3.2 github.com/glebarez/sqlite v1.10.0 github.com/go-co-op/gocron v1.37.0 + github.com/go-faster/errors v0.7.1 + github.com/go-faster/jx v1.2.0 github.com/google/uuid v1.6.0 github.com/jellydator/ttlcache/v3 v3.4.0 - github.com/julienschmidt/httprouter v1.3.0 - github.com/lib/pq v1.10.9 - github.com/pkg/errors v0.9.1 + github.com/ogen-go/ogen v1.20.3 github.com/prometheus/client_golang v1.22.0 github.com/rs/zerolog v1.34.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 google.golang.org/protobuf v1.36.6 gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 gopkg.in/yaml.v2 v2.4.0 @@ -40,7 +41,7 @@ require ( require ( github.com/OffchainLabs/prysm/v6 v6.0.5-0.20250610223352-6087875da526 // indirect - github.com/andybalholm/brotli v1.1.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.9 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect @@ -65,16 +66,20 @@ require ( github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dennis-tra/go-kinesis v0.0.0-20240326083914-7acf5f8dc24e // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/gosigar v0.14.3 // indirect github.com/emicklei/dot v1.8.0 // indirect github.com/ethereum/go-ethereum v1.16.1 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-faster/yaml v0.4.6 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-yaml v1.17.1 // indirect @@ -99,7 +104,7 @@ require ( github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect @@ -158,6 +163,7 @@ require ( github.com/pion/turn/v4 v4.0.1 // indirect github.com/pion/webrtc/v4 v4.1.0 // indirect github.com/pk910/dynamic-ssz v0.0.6 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/probe-lab/hermes v0.0.0-20250328140724-f552d3382c38 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -174,20 +180,19 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/thejerf/suture/v4 v4.0.6 // indirect github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e // indirect github.com/wlynxg/anet v0.0.5 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect @@ -195,16 +200,17 @@ require ( go.uber.org/fx v1.23.0 // indirect go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.38.0 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect google.golang.org/grpc v1.72.1 // indirect @@ -217,3 +223,5 @@ require ( modernc.org/memory v1.7.2 // indirect modernc.org/sqlite v1.28.0 // indirect ) + +tool github.com/ogen-go/ogen/cmd/ogen diff --git a/go.sum b/go.sum index bff2493..645672a 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/attestantio/go-eth2-client v0.26.1-0.20250721122214-dc2928832acc h1:rLrx/o7WeRqmE9iXvhyBMDrugOPZRhNBLQD/4XbG6hU= github.com/attestantio/go-eth2-client v0.26.1-0.20250721122214-dc2928832acc/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= @@ -144,6 +144,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dennis-tra/go-kinesis v0.0.0-20240326083914-7acf5f8dc24e h1:y6QyYh8YZyRQDdfnQqUgC5tRBmEwUFAjavnybKboCm4= github.com/dennis-tra/go-kinesis v0.0.0-20240326083914-7acf5f8dc24e/go.mod h1:5Hm3EOeNP1/lYm9qcFwWgYgjixQilwcZA+hZ05bUz54= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -167,6 +169,8 @@ github.com/ethpandaops/hermes v0.0.4-0.20250715094801-db6021d1c703 h1:kgbDt5VGau github.com/ethpandaops/hermes v0.0.4-0.20250715094801-db6021d1c703/go.mod h1:btvQ5XIwNLN1xKq0/G4HE+wxa7C93IFnxqfkM4PjMZA= github.com/ethpandaops/xatu v1.3.2 h1:2ieq1Db9GoAFngv6u/HCWo8U3Cz6BAAqY2UPF+7Macg= github.com/ethpandaops/xatu v1.3.2/go.mod h1:kld9Upqa0DRoNNyKzufXpYvY1YAnEU3s4ATJb1MaY94= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -175,6 +179,7 @@ github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwU github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= @@ -184,6 +189,12 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI= +github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE= +github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I= +github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -197,8 +208,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -344,13 +355,12 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -370,8 +380,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= @@ -459,6 +467,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/ogen-go/ogen v1.20.3 h1:1tvJuJE0BnQ7Nukd6ykiTOP0ucfL0yrAjHUg3S1DCQk= +github.com/ogen-go/ogen v1.20.3/go.mod h1:sJ1pJVp4S1RcSZlYIiMLo0QSMSt2pls4zfrc+hNKnzk= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= @@ -606,7 +616,11 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= @@ -657,8 +671,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8= github.com/thejerf/suture/v4 v4.0.6/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU= @@ -685,24 +699,24 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk= go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -720,8 +734,8 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -739,8 +753,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -776,8 +790,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -824,8 +838,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -853,8 +867,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -914,8 +928,10 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -934,8 +950,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -989,8 +1005,8 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/forky/http.go b/http.go similarity index 100% rename from pkg/forky/http.go rename to http.go diff --git a/pkg/forky/api/config.go b/internal/api/config.go similarity index 80% rename from pkg/forky/api/config.go rename to internal/api/config.go index a1572be..e8460df 100644 --- a/pkg/forky/api/config.go +++ b/internal/api/config.go @@ -1,8 +1,10 @@ package api import ( - "github.com/ethpandaops/forky/pkg/forky/human" - "github.com/pkg/errors" + "errors" + "fmt" + + "github.com/ethpandaops/forky/internal/human" ) type Config struct { @@ -17,7 +19,7 @@ type EdgeCacheConfig struct { func (c *Config) Validate() error { if err := c.EdgeCacheConfig.Validate(); err != nil { - return errors.Wrap(err, "invalid edge cache config") + return fmt.Errorf("invalid edge cache config: %w", err) } return nil diff --git a/internal/api/ethereum.go b/internal/api/ethereum.go new file mode 100644 index 0000000..a0c8200 --- /dev/null +++ b/internal/api/ethereum.go @@ -0,0 +1,44 @@ +package api + +import ( + "context" + + "github.com/ethpandaops/forky/api/rest" +) + +// GetEthereumNow implements getEthereumNow: the current wall-clock slot and +// epoch. +func (h *HTTP) GetEthereumNow(ctx context.Context) (rest.GetEthereumNowRes, error) { + slot, epoch, err := h.svc.GetEthereumNow(ctx) + if err != nil { + return nil, err + } + + return &rest.GetEthereumNowOKHeaders{ + CacheControl: rest.NewOptString("public, max-age=1, s-maxage=1"), + Response: rest.GetEthereumNowOK{ + Data: rest.EthereumNow{ + Slot: rest.Slot(safeInt64(uint64(slot))), + Epoch: rest.Epoch(safeInt64(uint64(epoch))), + }, + }, + }, nil +} + +// GetEthereumSpec implements getEthereumSpec: the configured network name and +// spec. +func (h *HTTP) GetEthereumSpec(ctx context.Context) (rest.GetEthereumSpecRes, error) { + return &rest.GetEthereumSpecOKHeaders{ + CacheControl: rest.NewOptString("public, max-age=60, s-maxage=60"), + Response: rest.GetEthereumSpecOK{ + Data: rest.EthereumSpecResult{ + NetworkName: h.svc.GetEthereumNetworkName(ctx), + Spec: rest.EthereumSpec{ + SecondsPerSlot: safeInt64(h.svc.GetEthereumSpecSecondsPerSlot(ctx)), + SlotsPerEpoch: safeInt64(h.svc.GetEthereumSpecSlotsPerEpoch(ctx)), + GenesisTime: h.svc.GetEthereumSpecGenesisTime(ctx).UTC(), + }, + }, + }, + }, nil +} diff --git a/internal/api/frames.go b/internal/api/frames.go new file mode 100644 index 0000000..2c804d1 --- /dev/null +++ b/internal/api/frames.go @@ -0,0 +1,138 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/go-faster/jx" + + "github.com/ethpandaops/forky/api/rest" + "github.com/ethpandaops/forky/internal/service" + "github.com/ethpandaops/forky/internal/types" +) + +// GetFrame implements getFrame: a single fork-choice frame by ID. +func (h *HTTP) GetFrame(ctx context.Context, params rest.GetFrameParams) (rest.GetFrameRes, error) { + frame, err := h.svc.GetFrame(ctx, params.ID) + if err != nil { + if errors.Is(err, service.ErrFrameNotFound) { + return &rest.Error{ + Code: http.StatusNotFound, + Message: err.Error(), + }, nil + } + + return nil, err + } + + restFrame, err := frameToRest(frame) + if err != nil { + return nil, err + } + + rsp := &rest.GetFrameOKHeaders{ + Response: rest.GetFrameOK{ + Data: rest.GetFrameOKData{ + Frame: restFrame, + }, + }, + } + + if h.config.EdgeCacheConfig.Enabled { + ttl := int64(h.config.EdgeCacheConfig.FrameTTL.Seconds()) + rsp.CacheControl = rest.NewOptString(fmt.Sprintf("public, max-age=%[1]d, s-maxage=%[1]d", ttl)) + } + + return rsp, nil +} + +// frameToRest maps a domain frame onto the spec's Frame schema. +func frameToRest(frame *types.Frame) (rest.Frame, error) { + data, err := forkChoiceToRest(frame.Data) + if err != nil { + return rest.Frame{}, err + } + + return rest.Frame{ + Data: data, + Metadata: frameMetadataToRest(&frame.Metadata), + }, nil +} + +// frameMetadataToRest maps domain frame metadata onto the spec's +// FrameMetadata schema. +func frameMetadataToRest(m *types.FrameMetadata) rest.FrameMetadata { + labels := m.Labels + if labels == nil { + labels = []string{} + } + + return rest.FrameMetadata{ + ID: m.ID, + Node: m.Node, + FetchedAt: m.FetchedAt.UTC(), + WallClockSlot: rest.Slot(safeInt64(uint64(m.WallClockSlot))), + WallClockEpoch: rest.Epoch(safeInt64(uint64(m.WallClockEpoch))), + Labels: labels, + ConsensusClient: m.ConsensusClient, + EventSource: m.EventSource, + } +} + +// forkChoiceToRest maps a beacon node fork-choice dump onto the spec's +// ForkChoice schema, following the beacon-API string serialization for +// slots, epochs and weights. +func forkChoiceToRest(fc *v1.ForkChoice) (rest.ForkChoice, error) { + nodes := make([]rest.ForkChoiceNode, 0, len(fc.ForkChoiceNodes)) + + for _, node := range fc.ForkChoiceNodes { + restNode := rest.ForkChoiceNode{ + Slot: rest.SlotString(strconv.FormatUint(uint64(node.Slot), 10)), + BlockRoot: rest.Root(node.BlockRoot.String()), + ParentRoot: rest.Root(node.ParentRoot.String()), + JustifiedEpoch: rest.EpochString(strconv.FormatUint(uint64(node.JustifiedEpoch), 10)), + FinalizedEpoch: rest.EpochString(strconv.FormatUint(uint64(node.FinalizedEpoch), 10)), + Weight: strconv.FormatUint(node.Weight, 10), + Validity: rest.ForkChoiceNodeValidity(node.Validity.String()), + ExecutionBlockHash: rest.Root(node.ExecutionBlockHash.String()), + } + + if len(node.ExtraData) > 0 { + extra := make(rest.ForkChoiceNodeExtraData, len(node.ExtraData)) + + for key, value := range node.ExtraData { + raw, err := json.Marshal(value) + if err != nil { + return rest.ForkChoice{}, fmt.Errorf("failed to marshal extra_data %q: %w", key, err) + } + + extra[key] = jx.Raw(raw) + } + + restNode.ExtraData = rest.NewOptForkChoiceNodeExtraData(extra) + } + + nodes = append(nodes, restNode) + } + + return rest.ForkChoice{ + JustifiedCheckpoint: checkpointToRest(fc.JustifiedCheckpoint), + FinalizedCheckpoint: checkpointToRest(fc.FinalizedCheckpoint), + ForkChoiceNodes: nodes, + }, nil +} + +// checkpointToRest maps a beacon checkpoint onto the spec's Checkpoint +// schema. +func checkpointToRest(cp phase0.Checkpoint) rest.Checkpoint { + return rest.Checkpoint{ + Epoch: rest.EpochString(strconv.FormatUint(uint64(cp.Epoch), 10)), + Root: rest.Root(cp.Root.String()), + } +} diff --git a/internal/api/http.go b/internal/api/http.go new file mode 100644 index 0000000..eb9756c --- /dev/null +++ b/internal/api/http.go @@ -0,0 +1,108 @@ +// Package api implements the Forky HTTP API as defined by the OpenAPI spec +// at api/openapi.yaml. The handlers implement the ogen-generated +// rest.Handler interface and map domain types onto the generated schema +// types, so the wire format is enforced by the spec. +package api + +import ( + "context" + "errors" + "fmt" + "math" + "net/http" + + "github.com/go-faster/jx" + "github.com/ogen-go/ogen/ogenerrors" + "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/api/rest" + "github.com/ethpandaops/forky/internal/service" +) + +// HTTP serves the Forky API over HTTP. +type HTTP struct { + log logrus.FieldLogger + svc *service.ForkChoice + metrics Metrics + config *Config + opts *Options +} + +var _ rest.Handler = (*HTTP)(nil) + +// NewHTTP creates a new HTTP API around the fork-choice service. +func NewHTTP(log logrus.FieldLogger, svc *service.ForkChoice, config *Config, opts *Options) (*HTTP, error) { + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid http config: %w", err) + } + + return &HTTP{ + opts: opts, + config: config, + svc: svc, + log: log.WithField("component", "http"), + metrics: NewMetrics(opts.MetricsEnabled, "http"), + }, nil +} + +// Handler returns the http.Handler serving the API, instrumented with +// per-route metrics. +func (h *HTTP) Handler() (http.Handler, error) { + srv, err := rest.NewServer(h, rest.WithErrorHandler(h.handleRequestError)) + if err != nil { + return nil, fmt.Errorf("failed to create rest server: %w", err) + } + + return h.instrument(srv), nil +} + +// NewError maps handler errors onto the spec's default error response. +func (h *HTTP) NewError(_ context.Context, err error) *rest.UnexpectedErrorStatusCode { + code := http.StatusInternalServerError + if errors.Is(err, service.ErrFrameNotFound) { + code = http.StatusNotFound + } + + if code >= http.StatusInternalServerError { + h.log.WithError(err).Error("Handler error") + } + + return &rest.UnexpectedErrorStatusCode{ + StatusCode: code, + Response: rest.Error{ + Code: code, + Message: err.Error(), + }, + } +} + +// handleRequestError writes ogen-level errors (request decoding, routing) +// using the spec's error envelope. +func (h *HTTP) handleRequestError(_ context.Context, w http.ResponseWriter, _ *http.Request, err error) { + code := ogenerrors.ErrorCode(err) + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(code) + + e := new(jx.Encoder) + + rsp := rest.Error{ + Code: code, + Message: err.Error(), + } + rsp.Encode(e) + + if _, writeErr := e.WriteTo(w); writeErr != nil { + h.log.WithError(writeErr).Error("Failed to write error response") + } +} + +// safeInt64 converts an unsigned value to int64, clamping at the maximum +// rather than overflowing. +func safeInt64(v uint64) int64 { + if v > math.MaxInt64 { + return math.MaxInt64 + } + + return int64(v) +} diff --git a/internal/api/metadata.go b/internal/api/metadata.go new file mode 100644 index 0000000..9be9c8d --- /dev/null +++ b/internal/api/metadata.go @@ -0,0 +1,204 @@ +package api + +import ( + "context" + + "github.com/ethpandaops/forky/api/rest" + "github.com/ethpandaops/forky/internal/service" +) + +// cacheControlNoStore disables caching for listing endpoints whose results +// change as new frames arrive. +const cacheControlNoStore = "private, max-age=0, no-cache, no-store, must-revalidate" + +// ListMetadata implements listMetadata: a page of frame metadata. +func (h *HTTP) ListMetadata(ctx context.Context, req *rest.MetadataQuery) (rest.ListMetadataRes, error) { + filter, page := metadataQueryFromRest(req) + + frames, pg, err := h.svc.ListMetadata(ctx, filter, page) + if err != nil { + return nil, err + } + + restFrames := make([]rest.FrameMetadata, 0, len(frames)) + for _, frame := range frames { + restFrames = append(restFrames, frameMetadataToRest(frame)) + } + + return &rest.ListMetadataOK{ + Data: rest.ListMetadataOKData{ + Frames: restFrames, + Pagination: paginationToRest(pg), + }, + }, nil +} + +// ListMetadataNodes implements listMetadataNodes: distinct node names. +func (h *HTTP) ListMetadataNodes(ctx context.Context, req *rest.MetadataQuery) (rest.ListMetadataNodesRes, error) { + filter, page := metadataQueryFromRest(req) + + nodes, pg, err := h.svc.ListNodes(ctx, filter, page) + if err != nil { + return nil, err + } + + if nodes == nil { + nodes = []string{} + } + + return &rest.ListMetadataNodesOKHeaders{ + CacheControl: rest.NewOptString(cacheControlNoStore), + Response: rest.ListMetadataNodesOK{ + Data: rest.ListMetadataNodesOKData{ + Nodes: nodes, + Pagination: paginationToRest(pg), + }, + }, + }, nil +} + +// ListMetadataSlots implements listMetadataSlots: distinct slots. +func (h *HTTP) ListMetadataSlots(ctx context.Context, req *rest.MetadataQuery) (rest.ListMetadataSlotsRes, error) { + filter, page := metadataQueryFromRest(req) + + slots, pg, err := h.svc.ListSlots(ctx, filter, page) + if err != nil { + return nil, err + } + + restSlots := make([]rest.Slot, 0, len(slots)) + for _, slot := range slots { + restSlots = append(restSlots, rest.Slot(safeInt64(uint64(slot)))) + } + + return &rest.ListMetadataSlotsOKHeaders{ + CacheControl: rest.NewOptString(cacheControlNoStore), + Response: rest.ListMetadataSlotsOK{ + Data: rest.ListMetadataSlotsOKData{ + Slots: restSlots, + Pagination: paginationToRest(pg), + }, + }, + }, nil +} + +// ListMetadataEpochs implements listMetadataEpochs: distinct epochs. +func (h *HTTP) ListMetadataEpochs(ctx context.Context, req *rest.MetadataQuery) (rest.ListMetadataEpochsRes, error) { + filter, page := metadataQueryFromRest(req) + + epochs, pg, err := h.svc.ListEpochs(ctx, filter, page) + if err != nil { + return nil, err + } + + restEpochs := make([]rest.Epoch, 0, len(epochs)) + for _, epoch := range epochs { + restEpochs = append(restEpochs, rest.Epoch(safeInt64(uint64(epoch)))) + } + + return &rest.ListMetadataEpochsOKHeaders{ + CacheControl: rest.NewOptString(cacheControlNoStore), + Response: rest.ListMetadataEpochsOK{ + Data: rest.ListMetadataEpochsOKData{ + Epochs: restEpochs, + Pagination: paginationToRest(pg), + }, + }, + }, nil +} + +// ListMetadataLabels implements listMetadataLabels: distinct labels. +func (h *HTTP) ListMetadataLabels(ctx context.Context, req *rest.MetadataQuery) (rest.ListMetadataLabelsRes, error) { + filter, page := metadataQueryFromRest(req) + + labels, pg, err := h.svc.ListLabels(ctx, filter, page) + if err != nil { + return nil, err + } + + if labels == nil { + labels = []string{} + } + + return &rest.ListMetadataLabelsOKHeaders{ + CacheControl: rest.NewOptString(cacheControlNoStore), + Response: rest.ListMetadataLabelsOK{ + Data: rest.ListMetadataLabelsOKData{ + Labels: labels, + Pagination: paginationToRest(pg), + }, + }, + }, nil +} + +// metadataQueryFromRest maps the spec's MetadataQuery onto the service +// filter and pagination types. +func metadataQueryFromRest(req *rest.MetadataQuery) (*service.FrameFilter, service.PaginationCursor) { + filter := &service.FrameFilter{} + if f, ok := req.Filter.Get(); ok { + filter = frameFilterFromRest(f) + } + + page := *service.DefaultPagination() + + if p, ok := req.Pagination.Get(); ok { + page.Offset = p.Offset.Or(page.Offset) + page.Limit = p.Limit.Or(page.Limit) + } + + return filter, page +} + +// frameFilterFromRest maps the spec's FrameFilter onto the service filter. +func frameFilterFromRest(f rest.FrameFilter) *service.FrameFilter { + filter := &service.FrameFilter{ + Node: optStringPtr(f.Node), + ConsensusClient: optStringPtr(f.ConsensusClient), + EventSource: optStringPtr(f.EventSource), + } + + if v, ok := f.Before.Get(); ok { + filter.Before = &v + } + + if v, ok := f.After.Get(); ok { + filter.After = &v + } + + if v, ok := f.Slot.Get(); ok && v >= 0 { + slot := uint64(v) + filter.Slot = &slot + } + + if v, ok := f.Epoch.Get(); ok && v >= 0 { + epoch := uint64(v) + filter.Epoch = &epoch + } + + if f.Labels != nil { + labels := f.Labels + filter.Labels = &labels + } + + return filter +} + +// optStringPtr converts an optional string to the pointer form used by the +// service filter. +func optStringPtr(o rest.OptString) *string { + if v, ok := o.Get(); ok { + return &v + } + + return nil +} + +// paginationToRest maps the service pagination response onto the spec's +// PaginationResponse schema. +func paginationToRest(pg *service.PaginationResponse) rest.PaginationResponse { + if pg == nil { + return rest.PaginationResponse{} + } + + return rest.PaginationResponse{Total: pg.Total} +} diff --git a/pkg/forky/api/http/metrics.go b/internal/api/metrics.go similarity index 54% rename from pkg/forky/api/http/metrics.go rename to internal/api/metrics.go index 410b4bc..f131066 100644 --- a/pkg/forky/api/http/metrics.go +++ b/internal/api/metrics.go @@ -1,17 +1,26 @@ -package http +package api import ( + "net/http" + "strconv" "time" "github.com/prometheus/client_golang/prometheus" + + "github.com/ethpandaops/forky/api/rest" ) +const encodingJSON = "application/json" + +// Metrics records request/response counts and durations per route. type Metrics struct { requests *prometheus.CounterVec responses *prometheus.CounterVec requestDuration *prometheus.HistogramVec } +// NewMetrics creates the API metrics collectors, registering them when +// enabled. func NewMetrics(enabled bool, namespace string) Metrics { m := Metrics{ requests: prometheus.NewCounterVec(prometheus.CounterOpts{ @@ -41,11 +50,44 @@ func NewMetrics(enabled bool, namespace string) Metrics { return m } +// ObserveRequest records an incoming request. func (m Metrics) ObserveRequest(method, path string) { m.requests.WithLabelValues(method, path).Inc() } +// ObserveResponse records a completed response. func (m Metrics) ObserveResponse(method, path, code, encoding string, duration time.Duration) { m.responses.WithLabelValues(method, path, code, encoding).Inc() m.requestDuration.WithLabelValues(method, path, encoding).Observe(duration.Seconds()) } + +// statusRecorder captures the response status code for metrics. +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (w *statusRecorder) WriteHeader(status int) { + w.status = status + w.ResponseWriter.WriteHeader(status) +} + +// instrument wraps the generated server with per-route metrics, using the +// spec's path pattern as the label to keep cardinality bounded. +func (h *HTTP) instrument(srv *rest.Server) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if route, ok := srv.FindRoute(r.Method, r.URL.Path); ok { + path = route.PathPattern() + } + + h.metrics.ObserveRequest(r.Method, path) + + start := time.Now() + rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} + + srv.ServeHTTP(rec, r) + + h.metrics.ObserveResponse(r.Method, path, strconv.Itoa(rec.status), encodingJSON, time.Since(start)) + }) +} diff --git a/pkg/forky/api/option.go b/internal/api/option.go similarity index 100% rename from pkg/forky/api/option.go rename to internal/api/option.go diff --git a/pkg/forky/db/config.go b/internal/db/config.go similarity index 100% rename from pkg/forky/db/config.go rename to internal/db/config.go diff --git a/pkg/forky/db/event_source.go b/internal/db/event_source.go similarity index 95% rename from pkg/forky/db/event_source.go rename to internal/db/event_source.go index 1a5a675..6056a32 100644 --- a/pkg/forky/db/event_source.go +++ b/internal/db/event_source.go @@ -1,6 +1,6 @@ package db -import "github.com/ethpandaops/forky/pkg/forky/types" +import "github.com/ethpandaops/forky/internal/types" type EventSource int diff --git a/pkg/forky/db/filters.go b/internal/db/filters.go similarity index 100% rename from pkg/forky/db/filters.go rename to internal/db/filters.go diff --git a/pkg/forky/db/frame_metadata.go b/internal/db/frame_metadata.go similarity index 97% rename from pkg/forky/db/frame_metadata.go rename to internal/db/frame_metadata.go index 17425c3..1a89189 100644 --- a/pkg/forky/db/frame_metadata.go +++ b/internal/db/frame_metadata.go @@ -4,8 +4,9 @@ import ( "time" "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethpandaops/forky/pkg/forky/types" "gorm.io/gorm" + + "github.com/ethpandaops/forky/internal/types" ) type FrameMetadata struct { diff --git a/pkg/forky/db/frame_metadata_label.go b/internal/db/frame_metadata_label.go similarity index 100% rename from pkg/forky/db/frame_metadata_label.go rename to internal/db/frame_metadata_label.go diff --git a/pkg/forky/db/indexer.go b/internal/db/indexer.go similarity index 97% rename from pkg/forky/db/indexer.go rename to internal/db/indexer.go index b4d186f..b451bb1 100644 --- a/pkg/forky/db/indexer.go +++ b/internal/db/indexer.go @@ -7,13 +7,13 @@ import ( "time" "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/glebarez/sqlite" - perrors "github.com/pkg/errors" "github.com/sirupsen/logrus" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/plugin/prometheus" + + "github.com/ethpandaops/forky/internal/types" ) type Indexer struct { @@ -59,17 +59,17 @@ func NewIndexer(namespace string, log logrus.FieldLogger, config IndexerConfig, StartServer: false, }), ); err != nil { - return nil, perrors.Wrap(err, "failed to register prometheus plugin") + return nil, fmt.Errorf("failed to register prometheus plugin: %w", err) } err = db.AutoMigrate(&FrameMetadata{}) if err != nil { - return nil, perrors.Wrap(err, "failed to auto migrate frame_metadata") + return nil, fmt.Errorf("failed to auto migrate frame_metadata: %w", err) } err = db.AutoMigrate(&FrameMetadataLabels{}) if err != nil { - return nil, perrors.Wrap(err, "failed to auto migrate frame_metadata_label") + return nil, fmt.Errorf("failed to auto migrate frame_metadata_label: %w", err) } return &Indexer{ @@ -556,7 +556,7 @@ func (i *Indexer) DeleteFrameMetadataLabelsByName(ctx context.Context, name stri query := i.db.WithContext(ctx) - result := query.Unscoped().Where("name LIKE ?", fmt.Sprintf("%s=%%", name)).Delete(&FrameMetadataLabels{}) + result := query.Unscoped().Where("name LIKE ?", name+"=%").Delete(&FrameMetadataLabels{}) if result.Error != nil { i.metrics.ObserveOperationError(operation) @@ -607,6 +607,7 @@ func (i *Indexer) getFrameIDsWithLabels(ctx context.Context, labels []string, pa } query := i.db. + WithContext(ctx). Model(&FrameMetadataLabel{}). Where("name IN (?)", labels). Limit(page.Limit). diff --git a/pkg/forky/db/indexer_test.go b/internal/db/indexer_test.go similarity index 97% rename from pkg/forky/db/indexer_test.go rename to internal/db/indexer_test.go index e464e86..c542a81 100644 --- a/pkg/forky/db/indexer_test.go +++ b/internal/db/indexer_test.go @@ -9,11 +9,12 @@ import ( "time" "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "gopkg.in/DATA-DOG/go-sqlmock.v1" + + "github.com/ethpandaops/forky/internal/types" ) //nolint:gosec // not a security issue @@ -21,14 +22,12 @@ func testRandIntn(n int) int { return rand.Intn(n) } -var ( - testDBCounter = 0 -) +var testDBCounter = 0 func newMockIndexer() (*Indexer, sqlmock.Sqlmock, error) { db, mock, err := sqlmock.New() if err != nil { - return nil, nil, fmt.Errorf("Failed to open mock sql db, got error: %v", err) + return nil, nil, fmt.Errorf("Failed to open mock sql db, got error: %w", err) } if db == nil { @@ -46,7 +45,7 @@ func newMockIndexer() (*Indexer, sqlmock.Sqlmock, error) { DriverName: "sqlite", }, DefaultOptions().SetMetricsEnabled(false)) if err != nil { - return nil, nil, fmt.Errorf("Failed to create indexer, got error: %v", err) + return nil, nil, fmt.Errorf("Failed to create indexer, got error: %w", err) } return indexer, mock, nil @@ -409,7 +408,7 @@ func TestIndexer_ListFrames(t *testing.T) { t.Fatal(err) } - assert.Len(t, frames, 0) + assert.Empty(t, frames) frames, err = indexer.ListFrameMetadata(context.Background(), &FrameFilter{ Labels: &[]string{"z", "d", "e", "f"}, @@ -437,7 +436,7 @@ func TestIndexer_ListFrames(t *testing.T) { } // Add 1000 random frames - for i := 0; i < 1000; i++ { + for i := range 1000 { id := uuid.New().String() node := fmt.Sprintf("node-%d", i) //nolint:gosec // ignore integer overflow conversion uint64 -> int64 @@ -469,7 +468,7 @@ func TestIndexer_ListFrames(t *testing.T) { } // List frames with random filters - for i := 0; i < 500; i++ { + for range 500 { var filter FrameFilter if testRandIntn(2) == 1 { @@ -585,7 +584,7 @@ func TestIndexer_ListFrames(t *testing.T) { } // Add a few random frames - for i := 0; i < 10; i++ { + for i := range 10 { frame := &types.FrameMetadata{ ID: uuid.New().String(), Node: fmt.Sprintf("node%d", i), @@ -632,7 +631,7 @@ func TestIndexer_ListNodesWithFrames(t *testing.T) { } // Add a few random frames - for i := 0; i < 5; i++ { + for i := range 5 { node := fmt.Sprintf("node%d", i) frame := &types.FrameMetadata{ @@ -676,7 +675,7 @@ func TestIndexer_ListNodesWithFrames(t *testing.T) { } // Add a few random frames - for i := 0; i < 5; i++ { + for i := range 5 { node := fmt.Sprintf("node%d", i) frame := &types.FrameMetadata{ @@ -715,7 +714,7 @@ func TestIndexer_ListNodesWithFrames(t *testing.T) { } for _, frame := range frames { - assert.Equal(t, frame.Labels[0].Name, "label5") + assert.Equal(t, "label5", frame.Labels[0].Name) } } }) @@ -731,7 +730,7 @@ func TestIndexer_DeleteFrameMetadata(t *testing.T) { ids := []string{} // Add a few random frames - for i := 0; i < 5; i++ { + for i := range 5 { node := fmt.Sprintf("node%d", i) frame := &types.FrameMetadata{ @@ -767,7 +766,7 @@ func TestIndexer_DeleteFrameMetadata(t *testing.T) { } // Check that no frame metadata is returned - assert.Len(t, nodes, 0) + assert.Empty(t, nodes) }) t.Run("with no labels", func(t *testing.T) { @@ -779,7 +778,7 @@ func TestIndexer_DeleteFrameMetadata(t *testing.T) { ids := []string{} // Add a few random frames - for i := 0; i < 5; i++ { + for i := range 5 { node := fmt.Sprintf("node%d", i) frame := &types.FrameMetadata{ @@ -815,6 +814,6 @@ func TestIndexer_DeleteFrameMetadata(t *testing.T) { } // Check that no frame metadata is returned - assert.Len(t, nodes, 0) + assert.Empty(t, nodes) }) } diff --git a/pkg/forky/db/metrics.go b/internal/db/metrics.go similarity index 100% rename from pkg/forky/db/metrics.go rename to internal/db/metrics.go diff --git a/pkg/forky/db/operation.go b/internal/db/operation.go similarity index 100% rename from pkg/forky/db/operation.go rename to internal/db/operation.go diff --git a/pkg/forky/db/option.go b/internal/db/option.go similarity index 100% rename from pkg/forky/db/option.go rename to internal/db/option.go diff --git a/pkg/forky/db/pagination.go b/internal/db/pagination.go similarity index 100% rename from pkg/forky/db/pagination.go rename to internal/db/pagination.go diff --git a/pkg/forky/ethereum/client.go b/internal/ethereum/client.go similarity index 100% rename from pkg/forky/ethereum/client.go rename to internal/ethereum/client.go diff --git a/pkg/forky/ethereum/config.go b/internal/ethereum/config.go similarity index 87% rename from pkg/forky/ethereum/config.go rename to internal/ethereum/config.go index 2b0979e..955446b 100644 --- a/pkg/forky/ethereum/config.go +++ b/internal/ethereum/config.go @@ -1,7 +1,8 @@ package ethereum import ( - "github.com/pkg/errors" + "errors" + "fmt" ) type Config struct { @@ -21,7 +22,7 @@ type SpecConfig struct { func (c *Config) Validate() error { if err := c.Network.Validate(); err != nil { - return errors.Wrap(err, "invalid network config") + return fmt.Errorf("invalid network config: %w", err) } return nil @@ -33,7 +34,7 @@ func (c *NetworkConfig) Validate() error { } if err := c.Spec.Validate(); err != nil { - return errors.Wrap(err, "invalid spec config") + return fmt.Errorf("invalid spec config: %w", err) } return nil diff --git a/pkg/forky/ethereum/ethereum.go b/internal/ethereum/ethereum.go similarity index 95% rename from pkg/forky/ethereum/ethereum.go rename to internal/ethereum/ethereum.go index 693f976..0a1268e 100644 --- a/pkg/forky/ethereum/ethereum.go +++ b/internal/ethereum/ethereum.go @@ -5,7 +5,7 @@ import ( "time" "github.com/ethpandaops/ethwallclock" - "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) @@ -24,7 +24,7 @@ func NewBeaconChain(log logrus.FieldLogger, config *Config) (*BeaconChain, error secondsPerSlot, err := time.ParseDuration(fmt.Sprintf("%vs", config.Network.Spec.SecondsPerSlot)) if err != nil { - return nil, errors.Wrap(err, "failed to parse seconds per slot") + return nil, fmt.Errorf("failed to parse seconds per slot: %w", err) } //nolint:gosec // ignore integer overflow conversion uint64 -> int64 diff --git a/pkg/forky/ethereum/ethereum_test.go b/internal/ethereum/ethereum_test.go similarity index 95% rename from pkg/forky/ethereum/ethereum_test.go rename to internal/ethereum/ethereum_test.go index 7cb6b52..135c1cf 100644 --- a/pkg/forky/ethereum/ethereum_test.go +++ b/internal/ethereum/ethereum_test.go @@ -6,6 +6,7 @@ import ( "github.com/ethpandaops/ethwallclock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewBeaconChain_ValidConfig_ReturnsBeaconChain(t *testing.T) { @@ -21,7 +22,7 @@ func TestNewBeaconChain_ValidConfig_ReturnsBeaconChain(t *testing.T) { } log := logrus.New() beaconchain, err := NewBeaconChain(log, config) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, beaconchain) } @@ -29,7 +30,7 @@ func TestNewBeaconChain_InvalidConfig_ReturnsError(t *testing.T) { config := &Config{} log := logrus.New() beaconchain, err := NewBeaconChain(log, config) - assert.Error(t, err) + require.Error(t, err) assert.Nil(t, beaconchain) } diff --git a/pkg/forky/human/duration.go b/internal/human/duration.go similarity index 87% rename from pkg/forky/human/duration.go rename to internal/human/duration.go index d1748c1..50d7cda 100644 --- a/pkg/forky/human/duration.go +++ b/internal/human/duration.go @@ -29,9 +29,9 @@ func (d *Duration) Unmarshal(s string) (err error) { } func (d Duration) MarshalText() ([]byte, error) { - return []byte(d.Duration.String()), nil + return []byte(d.String()), nil } func (d Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Duration.String()) + return json.Marshal(d.String()) } diff --git a/pkg/forky/service/backfill.go b/internal/service/backfill.go similarity index 93% rename from pkg/forky/service/backfill.go rename to internal/service/backfill.go index cec2a18..c0cda0e 100644 --- a/pkg/forky/service/backfill.go +++ b/internal/service/backfill.go @@ -6,8 +6,7 @@ import ( "strings" "time" - "github.com/ethpandaops/forky/pkg/forky/db" - "github.com/pkg/errors" + "github.com/ethpandaops/forky/internal/db" ) func (f *ForkChoice) BackfillConsensusClient(ctx context.Context) error { @@ -31,7 +30,6 @@ func (f *ForkChoice) BackfillConsensusClient(ctx context.Context) error { // Attempt to extract frame from labels. found := false - //nolint:gocritic // Outside of our control. for _, label := range frame.Labels { if strings.Contains(label.Name, "consensus_client_implementation") { frame.ConsensusClient = strings.Split(label.Name, "=")[1] @@ -82,7 +80,7 @@ func (f *ForkChoice) BackfillEventSource(ctx context.Context) error { for _, frame := range frames { // Attempt to extract frame from labels. - //nolint:gocritic // Outside of our control. + for _, label := range frame.Labels { // Default to beacon_node. frame.EventSource = db.BeaconNodeEventSource @@ -157,7 +155,7 @@ func (f *ForkChoice) DeleteUselessLabels(ctx context.Context) error { } { rowsAffected, err := f.indexer.DeleteFrameMetadataLabelsByName(ctx, unwanted) if err != nil { - return errors.Wrap(err, fmt.Sprintf("failed to delete labels by name %v", unwanted)) + return fmt.Errorf("failed to delete labels by name %v: %w", unwanted, err) } if rowsAffected > 0 { diff --git a/pkg/forky/service/config.go b/internal/service/config.go similarity index 54% rename from pkg/forky/service/config.go rename to internal/service/config.go index 53908d1..c8f103e 100644 --- a/pkg/forky/service/config.go +++ b/internal/service/config.go @@ -1,11 +1,11 @@ package service import ( - "github.com/ethpandaops/forky/pkg/forky/db" - "github.com/ethpandaops/forky/pkg/forky/ethereum" - "github.com/ethpandaops/forky/pkg/forky/human" - "github.com/ethpandaops/forky/pkg/forky/source" - "github.com/ethpandaops/forky/pkg/forky/store" + "github.com/ethpandaops/forky/internal/db" + "github.com/ethpandaops/forky/internal/ethereum" + "github.com/ethpandaops/forky/internal/human" + "github.com/ethpandaops/forky/internal/source" + "github.com/ethpandaops/forky/internal/store" ) type Config struct { diff --git a/pkg/forky/service/errors.go b/internal/service/errors.go similarity index 100% rename from pkg/forky/service/errors.go rename to internal/service/errors.go diff --git a/pkg/forky/service/forkchoice.go b/internal/service/forkchoice.go similarity index 96% rename from pkg/forky/service/forkchoice.go rename to internal/service/forkchoice.go index 8fc4c01..09237dd 100644 --- a/pkg/forky/service/forkchoice.go +++ b/internal/service/forkchoice.go @@ -4,7 +4,7 @@ import ( "errors" "time" - "github.com/ethpandaops/forky/pkg/forky/db" + "github.com/ethpandaops/forky/internal/db" ) type SourceMetadata struct { diff --git a/pkg/forky/service/metrics.go b/internal/service/metrics.go similarity index 95% rename from pkg/forky/service/metrics.go rename to internal/service/metrics.go index bb2c749..0ec86e9 100644 --- a/pkg/forky/service/metrics.go +++ b/internal/service/metrics.go @@ -3,8 +3,9 @@ package service import ( "runtime" - "github.com/ethpandaops/forky/pkg/version" "github.com/prometheus/client_golang/prometheus" + + "github.com/ethpandaops/forky/internal/version" ) type Metrics struct { @@ -59,7 +60,7 @@ func NewMetrics(namespace string, config *Config, enabled bool) *Metrics { prometheus.MustRegister(m.versionInfo) } - m.retentionPeriod.Set(config.RetentionPeriod.Duration.Seconds()) + m.retentionPeriod.Set(config.RetentionPeriod.Seconds()) m.info.WithLabelValues(version.FullVWithGOOS()).Set(1) m.versionInfo.WithLabelValues( diff --git a/pkg/forky/service/operations.go b/internal/service/operations.go similarity index 100% rename from pkg/forky/service/operations.go rename to internal/service/operations.go diff --git a/pkg/forky/service/option.go b/internal/service/option.go similarity index 100% rename from pkg/forky/service/option.go rename to internal/service/option.go diff --git a/pkg/forky/service/pagination.go b/internal/service/pagination.go similarity index 90% rename from pkg/forky/service/pagination.go rename to internal/service/pagination.go index 7ab2819..8336b2a 100644 --- a/pkg/forky/service/pagination.go +++ b/internal/service/pagination.go @@ -1,6 +1,6 @@ package service -import "github.com/ethpandaops/forky/pkg/forky/db" +import "github.com/ethpandaops/forky/internal/db" type PaginationCursor struct { // The cursor to start from. diff --git a/pkg/forky/service/purger.go b/internal/service/purger.go similarity index 95% rename from pkg/forky/service/purger.go rename to internal/service/purger.go index b3f2a89..d19cdb0 100644 --- a/pkg/forky/service/purger.go +++ b/internal/service/purger.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/ethpandaops/forky/pkg/forky/db" + "github.com/ethpandaops/forky/internal/db" ) func (f *ForkChoice) DeleteOldFrames(ctx context.Context) error { diff --git a/pkg/forky/service/service.go b/internal/service/service.go similarity index 95% rename from pkg/forky/service/service.go rename to internal/service/service.go index 8c8fc36..3d9b3b4 100644 --- a/pkg/forky/service/service.go +++ b/internal/service/service.go @@ -4,16 +4,18 @@ import ( "context" "errors" "fmt" + "strconv" "time" "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethpandaops/forky/pkg/forky/db" - "github.com/ethpandaops/forky/pkg/forky/ethereum" - "github.com/ethpandaops/forky/pkg/forky/source" - "github.com/ethpandaops/forky/pkg/forky/store" - "github.com/ethpandaops/forky/pkg/forky/types" - "github.com/ethpandaops/forky/pkg/version" "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/internal/db" + "github.com/ethpandaops/forky/internal/ethereum" + "github.com/ethpandaops/forky/internal/source" + "github.com/ethpandaops/forky/internal/store" + "github.com/ethpandaops/forky/internal/types" + "github.com/ethpandaops/forky/internal/version" ) type ForkChoice struct { @@ -39,7 +41,6 @@ func NewForkChoice(namespace string, log logrus.FieldLogger, config *Config, opt for _, s := range config.Sources { conf := s.Config sou, err := source.NewSource(namespace, log, s.Name, s.Type, conf, sourceOpts) - if err != nil { log.Fatalf("failed to create source %s: %s", s.Name, err) } @@ -52,7 +53,7 @@ func NewForkChoice(namespace string, log logrus.FieldLogger, config *Config, opt st, err := store.NewStore(namespace, log, config.Store.Type, config.Store.Config, storeOpts) if err != nil { - return nil, fmt.Errorf("failed to create store: %s", err) + return nil, fmt.Errorf("failed to create store: %w", err) } // Create our indexer. @@ -199,7 +200,7 @@ func (f *ForkChoice) AddNewFrame(ctx context.Context, sourceName string, frame * "source": sourceName, "id": frame.Metadata.ID, "slot": fmt.Sprintf("%v", frame.Metadata.WallClockSlot), - "fetchedAt": fmt.Sprintf("%v", frame.Metadata.FetchedAt.Unix()), + "fetchedAt": strconv.FormatInt(frame.Metadata.FetchedAt.Unix(), 10), "node": frame.Metadata.Node, }) @@ -413,7 +414,7 @@ func (f *ForkChoice) GetFrame(ctx context.Context, id string) (*types.Frame, err f.log.WithError(err).WithField("id", id).Error("failed to get frame") - if err == store.ErrFrameNotFound { + if errors.Is(err, store.ErrFrameNotFound) { return nil, ErrFrameNotFound } @@ -434,7 +435,7 @@ func (f *ForkChoice) DeleteFrame(ctx context.Context, id string) error { return ErrInvalidID } - if err := f.store.DeleteFrame(ctx, id); err != nil && err != store.ErrFrameNotFound { + if err := f.store.DeleteFrame(ctx, id); err != nil && !errors.Is(err, store.ErrFrameNotFound) { f.metrics.ObserveOperationError(operation) f.log.WithError(err).WithField("id", id).Error("failed to delete frame") diff --git a/pkg/forky/source/beacon_node.go b/internal/source/beacon_node.go similarity index 91% rename from pkg/forky/source/beacon_node.go rename to internal/source/beacon_node.go index eabbc6b..91b34f4 100644 --- a/pkg/forky/source/beacon_node.go +++ b/internal/source/beacon_node.go @@ -3,6 +3,7 @@ package source import ( "context" "errors" + "fmt" "time" eth2client "github.com/attestantio/go-eth2-client" @@ -12,14 +13,14 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/cenkalti/backoff/v4" "github.com/ethpandaops/ethwallclock" - "github.com/ethpandaops/forky/pkg/forky/ethereum" - "github.com/ethpandaops/forky/pkg/forky/store" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/go-co-op/gocron" "github.com/google/uuid" - perrors "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/internal/ethereum" + "github.com/ethpandaops/forky/internal/store" + "github.com/ethpandaops/forky/internal/types" ) var BeaconNodeType = "beacon_node" @@ -67,7 +68,7 @@ func (b *BeaconNodeConfig) Validate() error { func NewBeaconNode(namespace string, log logrus.FieldLogger, config *BeaconNodeConfig, name string, metrics *BasicMetrics) (*BeaconNode, error) { if err := config.Validate(); err != nil { - return nil, perrors.Wrap(err, "invalid config") + return nil, fmt.Errorf("invalid config: %w", err) } scheduler := gocron.NewScheduler(time.Local) @@ -99,7 +100,7 @@ func (b *BeaconNode) Start(ctx context.Context) error { } }) if err != nil { - return perrors.Wrap(err, "failed to schedule polling") + return fmt.Errorf("failed to schedule polling: %w", err) } go func() { @@ -173,7 +174,7 @@ func (b *BeaconNode) bootstrap(ctx context.Context) error { http.WithTimeout(15*time.Second), ) if err != nil { - return perrors.Wrap(err, "failed to create client") + return fmt.Errorf("failed to create client: %w", err) } b.client = client @@ -186,7 +187,7 @@ func (b *BeaconNode) bootstrap(ctx context.Context) error { // Fetch the genesis time and network parameters. rsp, err := genesisProvider.Genesis(ctx, &api.GenesisOpts{}) if err != nil { - return perrors.Wrap(err, "failed to fetch genesis time") + return fmt.Errorf("failed to fetch genesis time: %w", err) } if rsp == nil || rsp.Data == nil { @@ -203,7 +204,7 @@ func (b *BeaconNode) bootstrap(ctx context.Context) error { specRsp, err := specProvider.Spec(ctx, &api.SpecOpts{}) if err != nil { - return perrors.Wrap(err, "failed to fetch spec") + return fmt.Errorf("failed to fetch spec: %w", err) } if specRsp == nil || specRsp.Data == nil { @@ -254,7 +255,7 @@ func (b *BeaconNode) fetchFrame(ctx context.Context) error { slot, epoch, err := b.wallclock.Now() if err != nil { - return perrors.Wrap(err, "failed to get current wallclock") + return fmt.Errorf("failed to get current wallclock: %w", err) } nodeVersionProvider, ok := b.client.(eth2client.NodeVersionProvider) @@ -264,7 +265,7 @@ func (b *BeaconNode) fetchFrame(ctx context.Context) error { rsp, err := nodeVersionProvider.NodeVersion(ctx, &api.NodeVersionOpts{}) if err != nil { - return perrors.Wrap(err, "failed to get node version") + return fmt.Errorf("failed to get node version: %w", err) } nodeVersion := rsp.Data @@ -274,7 +275,7 @@ func (b *BeaconNode) fetchFrame(ctx context.Context) error { if provider, isProvider := b.client.(eth2client.ForkChoiceProvider); isProvider { rsp, err := provider.ForkChoice(ctx, &api.ForkChoiceOpts{}) if err != nil { - return perrors.Wrap(err, "failed to get fork choice dump") + return fmt.Errorf("failed to get fork choice dump: %w", err) } dump := rsp.Data diff --git a/pkg/forky/source/config.go b/internal/source/config.go similarity index 78% rename from pkg/forky/source/config.go rename to internal/source/config.go index 6c8c30c..aa93bc1 100644 --- a/pkg/forky/source/config.go +++ b/internal/source/config.go @@ -1,7 +1,7 @@ package source import ( - "github.com/ethpandaops/forky/pkg/yaml" + "github.com/ethpandaops/forky/internal/yaml" ) type Config struct { diff --git a/pkg/forky/source/error.go b/internal/source/error.go similarity index 100% rename from pkg/forky/source/error.go rename to internal/source/error.go diff --git a/pkg/forky/source/metrics.go b/internal/source/metrics.go similarity index 100% rename from pkg/forky/source/metrics.go rename to internal/source/metrics.go diff --git a/pkg/forky/source/option.go b/internal/source/option.go similarity index 100% rename from pkg/forky/source/option.go rename to internal/source/option.go diff --git a/pkg/forky/source/source.go b/internal/source/source.go similarity index 89% rename from pkg/forky/source/source.go rename to internal/source/source.go index bdf07eb..b8a44af 100644 --- a/pkg/forky/source/source.go +++ b/internal/source/source.go @@ -4,9 +4,10 @@ import ( "context" "fmt" - "github.com/ethpandaops/forky/pkg/forky/types" - "github.com/ethpandaops/forky/pkg/yaml" "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/internal/types" + "github.com/ethpandaops/forky/internal/yaml" ) type Source interface { @@ -23,8 +24,10 @@ type Source interface { OnFrame(func(ctx context.Context, frame *types.Frame)) } -var _ = Source(&BeaconNode{}) -var _ = Source(&XatuHTTP{}) +var ( + _ = Source(&BeaconNode{}) + _ = Source(&XatuHTTP{}) +) func NewSource(namespace string, log logrus.FieldLogger, name, sourceType string, config yaml.RawMessage, opts *Options) (Source, error) { namespace += "_source" diff --git a/pkg/forky/source/type.go b/internal/source/type.go similarity index 100% rename from pkg/forky/source/type.go rename to internal/source/type.go diff --git a/pkg/forky/source/xatu_http.go b/internal/source/xatu_http.go similarity index 93% rename from pkg/forky/source/xatu_http.go rename to internal/source/xatu_http.go index 4ee0393..e6a1bc2 100644 --- a/pkg/forky/source/xatu_http.go +++ b/internal/source/xatu_http.go @@ -6,16 +6,18 @@ import ( "fmt" "io" "net/http" + "slices" "strings" "time" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/ethpandaops/xatu/pkg/proto/xatu" "github.com/google/uuid" "github.com/sirupsen/logrus" "google.golang.org/protobuf/encoding/protojson" + + "github.com/ethpandaops/forky/internal/types" ) var XatuHTTPType = "xatu_http" @@ -175,9 +177,9 @@ func (x *XatuHTTP) handleNDJSONRequest(ctx context.Context, body []byte) error { events := []*xatu.DecoratedEvent{} - split := strings.Split(s, "\n") + split := strings.SplitSeq(s, "\n") - for _, line := range split { + for line := range split { if line == "" { continue } @@ -256,15 +258,7 @@ func (x *XatuHTTP) handleXatuEvent(ctx context.Context, event *xatu.DecoratedEve // Drop it if its from a network that we don't care about name := event.GetMeta().GetClient().GetEthereum().GetNetwork().GetName() - found := false - - for _, network := range x.opts.AllowedEthereumNetworks { - if network == name { - found = true - - break - } - } + found := slices.Contains(x.opts.AllowedEthereumNetworks, name) if !found { logCtx. @@ -284,16 +278,16 @@ func (x *XatuHTTP) handleXatuEvent(ctx context.Context, event *xatu.DecoratedEve return x.handleForkChoiceReorgEvent(ctx, event) case xatu.Event_BEACON_API_ETH_V1_DEBUG_FORK_CHOICE_REORG_V2: return x.handleForkChoiceReorgV2Event(ctx, event) + default: + return errors.New("unknown event type") // Should never happen (touch wood (tm (c))) } - - return errors.New("unknown event type") // Should never happen (touch wood (tm (c))) } func (x *XatuHTTP) handleForkChoiceEvent(ctx context.Context, event *xatu.DecoratedEvent) error { // Create a new frame based on the event fc := event.GetEthV1ForkChoiceV2() if fc == nil { - return fmt.Errorf("event is not a fork choice event") + return errors.New("event is not a fork choice event") } data, err := fc.AsGoEth2ClientV1ForkChoice() @@ -303,7 +297,7 @@ func (x *XatuHTTP) handleForkChoiceEvent(ctx context.Context, event *xatu.Decora additionalData := event.GetMeta().GetClient().GetEthV1DebugForkChoice() if additionalData == nil { - return fmt.Errorf("event is missing additional data") + return errors.New("event is missing additional data") } return x.createFrameFromSnapshotAndData(ctx, event, data, additionalData.GetSnapshot(), "") @@ -313,7 +307,7 @@ func (x *XatuHTTP) handleForkChoiceV2Event(ctx context.Context, event *xatu.Deco // Create a new frame based on the event fc := event.GetEthV1ForkChoiceV2() if fc == nil { - return fmt.Errorf("event is not a fork choice event") + return errors.New("event is not a fork choice event") } data, err := fc.AsGoEth2ClientV1ForkChoice() @@ -323,7 +317,7 @@ func (x *XatuHTTP) handleForkChoiceV2Event(ctx context.Context, event *xatu.Deco additionalData := event.GetMeta().GetClient().GetEthV1DebugForkChoiceV2() if additionalData == nil { - return fmt.Errorf("event is missing additional data") + return errors.New("event is missing additional data") } return x.createFrameFromSnapshotV2AndData(ctx, event, data, additionalData.GetSnapshot(), "") @@ -339,16 +333,16 @@ func (x *XatuHTTP) handleForkChoiceReorgEvent(ctx context.Context, event *xatu.D // Note: `before` can be nil if the reorg happened before the xatu sentry started fcr := event.GetEthV1ForkChoiceReorgV2() if fcr == nil { - return fmt.Errorf("event is not a fork choice reorg event") + return errors.New("event is not a fork choice reorg event") } additionalData := event.GetMeta().GetClient().GetEthV1DebugForkChoiceReorg() if additionalData == nil { - return fmt.Errorf("event is missing additional data") + return errors.New("event is missing additional data") } if fcr.After == nil && fcr.Before == nil { - return fmt.Errorf("event is missing both before and after data") + return errors.New("event is missing both before and after data") } if fcr.After != nil && additionalData.After != nil { @@ -388,16 +382,16 @@ func (x *XatuHTTP) handleForkChoiceReorgV2Event(ctx context.Context, event *xatu // Note: `before` can be nil if the reorg happened before the xatu sentry started fcr := event.GetEthV1ForkChoiceReorgV2() if fcr == nil { - return fmt.Errorf("event is not a fork choice reorg event") + return errors.New("event is not a fork choice reorg event") } additionalData := event.GetMeta().GetClient().GetEthV1DebugForkChoiceReorgV2() if additionalData == nil { - return fmt.Errorf("event is missing additional data") + return errors.New("event is missing additional data") } if fcr.After == nil && fcr.Before == nil { - return fmt.Errorf("event is missing both before and after data") + return errors.New("event is missing both before and after data") } if fcr.After != nil && additionalData.After != nil { diff --git a/pkg/forky/store/config.go b/internal/store/config.go similarity index 85% rename from pkg/forky/store/config.go rename to internal/store/config.go index 595fc50..a9ada42 100644 --- a/pkg/forky/store/config.go +++ b/internal/store/config.go @@ -3,7 +3,7 @@ package store import ( "fmt" - "github.com/ethpandaops/forky/pkg/yaml" + "github.com/ethpandaops/forky/internal/yaml" ) type Config struct { diff --git a/pkg/forky/store/errors.go b/internal/store/errors.go similarity index 100% rename from pkg/forky/store/errors.go rename to internal/store/errors.go diff --git a/pkg/forky/store/filesystem.go b/internal/store/filesystem.go similarity index 91% rename from pkg/forky/store/filesystem.go rename to internal/store/filesystem.go index 71831ce..ff463c5 100644 --- a/pkg/forky/store/filesystem.go +++ b/internal/store/filesystem.go @@ -7,7 +7,7 @@ import ( "os" "path/filepath" - "github.com/ethpandaops/forky/pkg/forky/types" + "github.com/ethpandaops/forky/internal/types" ) type FileSystem struct { @@ -23,7 +23,7 @@ type FileSystemConfig struct { // NewFileSystem creates a new FileSystem instance with the specified base directory. func NewFileSystem(namespace string, config FileSystemConfig, opts *Options) (*FileSystem, error) { if config.BaseDir == "" { - return nil, fmt.Errorf("base directory is required") + return nil, errors.New("base directory is required") } err := os.MkdirAll(config.BaseDir, 0o755) @@ -41,7 +41,7 @@ func NewFileSystem(namespace string, config FileSystemConfig, opts *Options) (*F } func (fs *FileSystem) framePath(id string) string { - return filepath.Join(fs.config.BaseDir, fmt.Sprintf("%s.json.gz", id)) + return filepath.Join(fs.config.BaseDir, id+".json.gz") } func (fs *FileSystem) SaveFrame(ctx context.Context, frame *types.Frame) error { diff --git a/pkg/forky/store/memory.go b/internal/store/memory.go similarity index 96% rename from pkg/forky/store/memory.go rename to internal/store/memory.go index 7115c82..22da0b0 100644 --- a/pkg/forky/store/memory.go +++ b/internal/store/memory.go @@ -4,8 +4,9 @@ import ( "context" "sync" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/internal/types" ) type MemoryStore struct { diff --git a/pkg/forky/store/metrics.go b/internal/store/metrics.go similarity index 100% rename from pkg/forky/store/metrics.go rename to internal/store/metrics.go diff --git a/pkg/forky/store/option.go b/internal/store/option.go similarity index 100% rename from pkg/forky/store/option.go rename to internal/store/option.go diff --git a/pkg/forky/store/s3.go b/internal/store/s3.go similarity index 88% rename from pkg/forky/store/s3.go rename to internal/store/s3.go index bf53e1f..afb5bb5 100644 --- a/pkg/forky/store/s3.go +++ b/internal/store/s3.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "errors" - "fmt" "path/filepath" "time" @@ -13,9 +12,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go" - "github.com/ethpandaops/forky/pkg/forky/types" "github.com/jellydator/ttlcache/v3" "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/internal/types" ) type S3Store struct { @@ -43,23 +43,16 @@ type S3StoreConfig struct { // NewS3Store creates a new S3Store instance with the specified AWS configuration, bucket name, and key prefix. func NewS3Store(namespace string, log logrus.FieldLogger, config *S3StoreConfig, opts *Options) (*S3Store, error) { - resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...any) (aws.Endpoint, error) { - return aws.Endpoint{ - PartitionID: "aws", - SigningRegion: config.Region, - URL: config.Endpoint, - HostnameImmutable: true, - }, nil - }) - cfg := aws.Config{ - Region: config.Region, - EndpointResolverWithOptions: resolver, - Credentials: credentials.NewStaticCredentialsProvider(config.AccessKey, config.AccessSecret, ""), + Region: config.Region, + Credentials: credentials.NewStaticCredentialsProvider(config.AccessKey, config.AccessSecret, ""), } s3Client := s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = config.UsePathStyle + if config.Endpoint != "" { + o.BaseEndpoint = aws.String(config.Endpoint) + } }) metrics := NewBasicMetrics(namespace, string(S3StoreType), opts.MetricsEnabled) @@ -199,5 +192,5 @@ func (s *S3Store) getFramesPath() string { } func (s *S3Store) getFilename(id string) string { - return fmt.Sprintf("%s.json.gz", id) + return id + ".json.gz" } diff --git a/pkg/forky/store/store.go b/internal/store/store.go similarity index 92% rename from pkg/forky/store/store.go rename to internal/store/store.go index 3bc28e1..bb468e0 100644 --- a/pkg/forky/store/store.go +++ b/internal/store/store.go @@ -4,9 +4,10 @@ import ( "context" "fmt" - "github.com/ethpandaops/forky/pkg/forky/types" - "github.com/ethpandaops/forky/pkg/yaml" "github.com/sirupsen/logrus" + + "github.com/ethpandaops/forky/internal/types" + "github.com/ethpandaops/forky/internal/yaml" ) // Store is an interface for different persistence implementations. diff --git a/pkg/forky/store/type.go b/internal/store/type.go similarity index 100% rename from pkg/forky/store/type.go rename to internal/store/type.go diff --git a/pkg/forky/types/event_source.go b/internal/types/event_source.go similarity index 100% rename from pkg/forky/types/event_source.go rename to internal/types/event_source.go diff --git a/pkg/forky/types/frame.go b/internal/types/frame.go similarity index 99% rename from pkg/forky/types/frame.go rename to internal/types/frame.go index ef4ee60..4bf4fdf 100644 --- a/pkg/forky/types/frame.go +++ b/internal/types/frame.go @@ -159,7 +159,7 @@ func GenerateFakeForkChoice() *v1.ForkChoice { numNodes := rand.Intn(10) + 1 // generate 1 to 10 fork choice nodes nodes := make([]*v1.ForkChoiceNode, numNodes) - for i := 0; i < numNodes; i++ { + for i := range numNodes { nodes[i] = &v1.ForkChoiceNode{ //nolint:gosec // This is a test function. Slot: phase0.Slot(rand.Uint64()), diff --git a/pkg/version/version.go b/internal/version/version.go similarity index 87% rename from pkg/version/version.go rename to internal/version/version.go index f6a3865..78bcb49 100644 --- a/pkg/version/version.go +++ b/internal/version/version.go @@ -11,7 +11,7 @@ var ( ) func Full() string { - return fmt.Sprintf("Forky/%s", Short()) + return "Forky/" + Short() } func Short() string { diff --git a/internal/yaml/raw_config.go b/internal/yaml/raw_config.go new file mode 100644 index 0000000..beb9af4 --- /dev/null +++ b/internal/yaml/raw_config.go @@ -0,0 +1,15 @@ +package yaml + +type RawMessage struct { + unmarshal func(any) error +} + +func (r *RawMessage) UnmarshalYAML(unmarshal func(any) error) error { + r.unmarshal = unmarshal + + return nil +} + +func (r *RawMessage) Unmarshal(v any) error { + return r.unmarshal(v) +} diff --git a/main.go b/main.go deleted file mode 100644 index c7d2d21..0000000 --- a/main.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "log" - "os" - "os/signal" - "syscall" - - //nolint:gosec // running pprof on a separate port - _ "net/http/pprof" - - "github.com/ethpandaops/forky/cmd" - _ "github.com/lib/pq" -) - -func main() { - cancel := make(chan os.Signal, 1) - signal.Notify(cancel, syscall.SIGTERM, syscall.SIGINT) - - go cmd.Execute() - - sig := <-cancel - log.Printf("Caught signal: %v", sig) - - os.Exit(0) -} diff --git a/makefile b/makefile deleted file mode 100644 index 853cfd6..0000000 --- a/makefile +++ /dev/null @@ -1,3 +0,0 @@ -build-web: - @echo "Building web frontend..." - @npm --prefix ./web install && npm --prefix ./web run build diff --git a/pkg/forky/api/ethereum.go b/pkg/forky/api/ethereum.go deleted file mode 100644 index e4eda72..0000000 --- a/pkg/forky/api/ethereum.go +++ /dev/null @@ -1,62 +0,0 @@ -package api - -import ( - "context" - "encoding/json" - "net/http" - - fhttp "github.com/ethpandaops/forky/pkg/forky/api/http" - - "github.com/julienschmidt/httprouter" -) - -func (h *HTTP) handleV1GetEthereumSpec(ctx context.Context, _ *http.Request, _ httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - rsp := fhttp.V1GetEthereumSpecResponse{ - NetworkName: h.svc.GetEthereumNetworkName(ctx), - Spec: fhttp.EthereumSpec{ - SecondsPerSlot: h.svc.GetEthereumSpecSecondsPerSlot(ctx), - SlotsPerEpoch: h.svc.GetEthereumSpecSlotsPerEpoch(ctx), - GenesisTime: h.svc.GetEthereumSpecGenesisTime(ctx), - }, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - response.SetCacheControl("public, max-age=60, s-maxage=60") - - return response, nil -} - -func (h *HTTP) handleV1GetEthereumNow(ctx context.Context, _ *http.Request, _ httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - slot, epoch, err := h.svc.GetEthereumNow(ctx) - if err != nil { - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1GetEthereumNowResponse{ - Slot: uint64(slot), - Epoch: uint64(epoch), - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - response.SetCacheControl("public, max-age=1, s-maxage=1") - - return response, nil -} diff --git a/pkg/forky/api/frames.go b/pkg/forky/api/frames.go deleted file mode 100644 index e8c4cf0..0000000 --- a/pkg/forky/api/frames.go +++ /dev/null @@ -1,55 +0,0 @@ -package api - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/pkg/errors" - - fhttp "github.com/ethpandaops/forky/pkg/forky/api/http" - "github.com/ethpandaops/forky/pkg/forky/service" - - "github.com/julienschmidt/httprouter" -) - -type GetFramesBatchRequest struct { - IDs []string `json:"ids"` -} - -func (h *HTTP) handleV1GetFrame(ctx context.Context, _ *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - id := p.ByName("id") - if id == "" { - return fhttp.NewBadRequestResponse(nil), errors.New("id is required") - } - - frame, err := h.svc.GetFrame(ctx, id) - if err != nil { - if errors.Is(err, service.ErrFrameNotFound) { - return fhttp.NewNotFoundResponse(nil), err - } - - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1GetFrameResponse{ - Frame: frame, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - if h.config.EdgeCacheConfig.Enabled { - response.SetCacheControl(fmt.Sprintf("public, max-age=%[1]v, s-maxage=%[1]v", h.config.EdgeCacheConfig.FrameTTL.Seconds())) - } - - return response, nil -} diff --git a/pkg/forky/api/http.go b/pkg/forky/api/http.go deleted file mode 100644 index dc52559..0000000 --- a/pkg/forky/api/http.go +++ /dev/null @@ -1,61 +0,0 @@ -package api - -import ( - // Added - "context" // Added - // Added - "net/http" - // Added - // Added - fhttp "github.com/ethpandaops/forky/pkg/forky/api/http" - "github.com/pkg/errors" - - "github.com/ethpandaops/forky/pkg/forky/service" - "github.com/julienschmidt/httprouter" - "github.com/sirupsen/logrus" -) - -type HTTP struct { - log logrus.FieldLogger - svc *service.ForkChoice - metrics *fhttp.Metrics - config *Config - opts *Options -} - -func NewHTTP(log logrus.FieldLogger, svc *service.ForkChoice, config *Config, opts *Options) (*HTTP, error) { - if err := config.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid http config") - } - - metrics := fhttp.NewMetrics(opts.MetricsEnabled, "http") - - return &HTTP{ - opts: opts, - config: config, - svc: svc, - log: log.WithField("component", "http"), - metrics: &metrics, - }, nil -} - -func (h *HTTP) BindToRouter(_ context.Context, router *httprouter.Router) error { - router.GET("/api/v1/ethereum/now", h.wrappedHandler(h.handleV1GetEthereumNow)) - router.GET("/api/v1/ethereum/spec", h.wrappedHandler(h.handleV1GetEthereumSpec)) - - router.GET("/api/v1/frames/:id", h.wrappedHandler(h.handleV1GetFrame)) - - router.POST("/api/v1/metadata", h.wrappedHandler(h.handleV1MetadataList)) - router.POST("/api/v1/metadata/nodes", h.wrappedHandler(h.handleV1MetadataListNodes)) - router.POST("/api/v1/metadata/slots", h.wrappedHandler(h.handleV1MetadataListSlots)) - router.POST("/api/v1/metadata/epochs", h.wrappedHandler(h.handleV1MetadataListEpochs)) - router.POST("/api/v1/metadata/labels", h.wrappedHandler(h.handleV1MetadataListLabels)) - - return nil -} - -func (h *HTTP) wrappedHandler(handler func(ctx context.Context, r *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error)) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - fhttp.WrappedHandler(h.log, h.metrics, handler)(w, r, p) - } -} diff --git a/pkg/forky/api/http/content_type.go b/pkg/forky/api/http/content_type.go deleted file mode 100644 index cb9f485..0000000 --- a/pkg/forky/api/http/content_type.go +++ /dev/null @@ -1,81 +0,0 @@ -package http - -import ( - "fmt" - "net/http" -) - -type ContentType int - -const ( - ContentTypeUnknown ContentType = iota - ContentTypeJSON - ContentTypeYAML - ContentTypeSSZ -) - -func (c ContentType) String() string { - switch c { - case ContentTypeJSON: - return "application/json" - case ContentTypeYAML: - return "application/yaml" - case ContentTypeSSZ: - return "application/octet-stream" - case ContentTypeUnknown: - return "unknown" - } - - return "" -} - -func DeriveContentType(accept string) ContentType { - switch accept { - case "application/json": - return ContentTypeJSON - case "*/*": - return ContentTypeJSON - case "application/yaml": - return ContentTypeYAML - case "application/octet-stream": - return ContentTypeSSZ - // Default to JSON if they don't care what they get. - case "": - return ContentTypeJSON - } - - return ContentTypeUnknown -} - -func ValidateContentType(contentType ContentType, accepting []ContentType) error { - if !DoesAccept(accepting, contentType) { - return fmt.Errorf("unsupported content-type: %s", contentType.String()) - } - - return nil -} - -func DoesAccept(accepts []ContentType, input ContentType) bool { - for _, a := range accepts { - if a == input { - return true - } - } - - return false -} - -func NewContentTypeFromRequest(r *http.Request) ContentType { - accept := r.Header.Get("Accept") - if accept == "" { - return ContentTypeJSON - } - - content := DeriveContentType(accept) - - if content == ContentTypeUnknown { - return ContentTypeJSON - } - - return content -} diff --git a/pkg/forky/api/http/error.go b/pkg/forky/api/http/error.go deleted file mode 100644 index b1d79f8..0000000 --- a/pkg/forky/api/http/error.go +++ /dev/null @@ -1,6 +0,0 @@ -package http - -type ErrorContainer struct { - Code int `json:"code"` - Message string `json:"message"` -} diff --git a/pkg/forky/api/http/http.go b/pkg/forky/api/http/http.go deleted file mode 100644 index 5fc3984..0000000 --- a/pkg/forky/api/http/http.go +++ /dev/null @@ -1,91 +0,0 @@ -package http - -import ( - "time" - - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethpandaops/forky/pkg/forky/service" - "github.com/ethpandaops/forky/pkg/forky/types" -) - -// V1 -// // Ethereum -type V1GetEthereumSpecRequest struct { -} - -type EthereumSpec struct { - SecondsPerSlot uint64 `json:"seconds_per_slot"` - SlotsPerEpoch uint64 `json:"slots_per_epoch"` - GenesisTime time.Time `json:"genesis_time"` -} -type V1GetEthereumSpecResponse struct { - NetworkName string `json:"network_name"` - Spec EthereumSpec `json:"spec"` -} - -type V1GetEthereumNowRequest struct { -} - -type V1GetEthereumNowResponse struct { - Slot uint64 `json:"slot"` - Epoch uint64 `json:"epoch"` -} - -// // Metadata -type V1MetadataListRequest struct { - Filter *service.FrameFilter `json:"filter"` - Pagination *service.PaginationCursor `json:"pagination"` -} - -type V1MetadataListResponse struct { - Frames []*types.FrameMetadata `json:"frames"` - Pagination *service.PaginationResponse `json:"pagination"` -} - -type V1MetadataListNodesRequest struct { - Filter *service.FrameFilter `json:"filter"` - Pagination *service.PaginationCursor `json:"pagination"` -} - -type V1MetadataListNodesResponse struct { - Nodes []string `json:"nodes"` - Pagination *service.PaginationResponse `json:"pagination"` -} - -type V1MetadataListSlotsRequest struct { - Filter *service.FrameFilter `json:"filter"` - Pagination *service.PaginationCursor `json:"pagination"` -} - -type V1MetadataListSlotsResponse struct { - Slots []phase0.Slot `json:"slots"` - Pagination *service.PaginationResponse `json:"pagination"` -} - -type V1MetadataListEpochsRequest struct { - Filter *service.FrameFilter `json:"filter"` - Pagination *service.PaginationCursor `json:"pagination"` -} - -type V1MetadataListEpochsResponse struct { - Epochs []phase0.Epoch `json:"epochs"` - Pagination *service.PaginationResponse `json:"pagination"` -} - -type V1MetadataListLabelsRequest struct { - Filter *service.FrameFilter `json:"filter"` - Pagination *service.PaginationCursor `json:"pagination"` -} - -type V1MetadataListLabelsResponse struct { - Labels []string `json:"labels"` - Pagination *service.PaginationResponse `json:"pagination"` -} - -// // Frames -type V1GetFrameRequest struct { -} - -type V1GetFrameResponse struct { - Frame *types.Frame `json:"frame"` -} diff --git a/pkg/forky/api/http/response.go b/pkg/forky/api/http/response.go deleted file mode 100644 index 60d6566..0000000 --- a/pkg/forky/api/http/response.go +++ /dev/null @@ -1,155 +0,0 @@ -package http - -import ( - "encoding/json" - "fmt" - "net/http" -) - -type ContentTypeResolver func() ([]byte, error) -type ContentTypeResolvers map[ContentType]ContentTypeResolver - -type Response struct { - resolvers ContentTypeResolvers - StatusCode int `json:"status_code"` - Headers map[string]string `json:"headers"` - ExtraData map[string]interface{} -} -type jsonResponse struct { - Data json.RawMessage `json:"data"` -} - -func (r Response) MarshalAs(contentType ContentType) ([]byte, error) { - if _, exists := r.resolvers[contentType]; !exists { - return nil, fmt.Errorf("unsupported content-type: %s", contentType.String()) - } - - if contentType != ContentTypeJSON { - return r.resolvers[contentType]() - } - - return r.buildWrappedJSONResponse() -} - -func (r Response) SetEtag(etag string) { - r.Headers["ETag"] = etag -} - -func (r Response) SetCacheControl(v string) { - r.Headers["Cache-Control"] = v -} - -func NewSuccessResponse(resolvers ContentTypeResolvers) *Response { - return &Response{ - resolvers: resolvers, - StatusCode: http.StatusOK, - Headers: make(map[string]string), - ExtraData: make(map[string]interface{}), - } -} - -func NewInternalServerErrorResponse(resolvers ContentTypeResolvers) *Response { - return &Response{ - resolvers: resolvers, - StatusCode: http.StatusInternalServerError, - Headers: make(map[string]string), - ExtraData: make(map[string]interface{}), - } -} - -func NewBadRequestResponse(resolvers ContentTypeResolvers) *Response { - return &Response{ - resolvers: resolvers, - StatusCode: http.StatusBadRequest, - Headers: make(map[string]string), - ExtraData: make(map[string]interface{}), - } -} - -func NewNotFoundResponse(resolvers ContentTypeResolvers) *Response { - return &Response{ - resolvers: resolvers, - StatusCode: http.StatusNotFound, - Headers: make(map[string]string), - ExtraData: make(map[string]interface{}), - } -} - -func NewUnsupportedMediaTypeResponse(resolvers ContentTypeResolvers) *Response { - return &Response{ - resolvers: resolvers, - StatusCode: http.StatusUnsupportedMediaType, - Headers: make(map[string]string), - ExtraData: make(map[string]interface{}), - } -} - -func (r *Response) AddExtraData(key string, value interface{}) { - r.ExtraData[key] = value -} - -func (r *Response) buildWrappedJSONResponse() ([]byte, error) { - data, err := r.resolvers[ContentTypeJSON]() - if err != nil { - return nil, err - } - - rsp := jsonResponse{ - Data: data, - } - - return json.Marshal(rsp) -} - -// WriteJSONResponse writes a JSON response to the given writer. -func WriteJSONResponse(w http.ResponseWriter, data []byte) error { - w.Header().Set("Content-Type", ContentTypeJSON.String()) - - if _, err := w.Write(data); err != nil { - return err - } - - return nil -} - -func WriteSSZResponse(w http.ResponseWriter, data []byte) error { - w.Header().Set("Content-Type", ContentTypeSSZ.String()) - - if _, err := w.Write(data); err != nil { - return err - } - - return nil -} - -func WriteContentAwareResponse(w http.ResponseWriter, data []byte, contentType ContentType) error { - switch contentType { - case ContentTypeJSON: - return WriteJSONResponse(w, data) - case ContentTypeSSZ: - return WriteSSZResponse(w, data) - default: - return WriteJSONResponse(w, data) - } -} - -func WriteErrorResponse(w http.ResponseWriter, msg string, statusCode int) error { - w.Header().Set("Content-Type", ContentTypeJSON.String()) - - w.WriteHeader(statusCode) - - bytes, err := json.Marshal( - ErrorContainer{ - Message: msg, - Code: statusCode, - }) - if err != nil { - return err - } - - if _, err := w.Write(bytes); err != nil { - return err - } - - return nil -} diff --git a/pkg/forky/api/http/routing.go b/pkg/forky/api/http/routing.go deleted file mode 100644 index e829c30..0000000 --- a/pkg/forky/api/http/routing.go +++ /dev/null @@ -1,90 +0,0 @@ -package http - -import ( - "context" - "fmt" - "net/http" - "strings" - "time" - - "github.com/julienschmidt/httprouter" - "github.com/sirupsen/logrus" -) - -func DeriveRegisteredPath(request *http.Request, ps httprouter.Params) string { - registeredPath := request.URL.Path - for _, param := range ps { - registeredPath = strings.Replace(registeredPath, param.Value, fmt.Sprintf(":%s", param.Key), 1) - } - - return registeredPath -} - -func WrappedHandler(log logrus.FieldLogger, metrics *Metrics, handler func(ctx context.Context, r *http.Request, p httprouter.Params, contentType ContentType) (*Response, error)) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - start := time.Now() - - contentType := NewContentTypeFromRequest(r) - ctx := r.Context() - registeredPath := DeriveRegisteredPath(r, p) - - log.WithFields(logrus.Fields{ - "method": r.Method, - "path": r.URL.Path, - "content_type": contentType, - "accept": r.Header.Get("Accept"), - }).Debug("Handling request") - - metrics.ObserveRequest(r.Method, registeredPath) - - response := &Response{} - - var err error - - defer func() { - metrics.ObserveResponse(r.Method, registeredPath, fmt.Sprintf("%v", response.StatusCode), contentType.String(), time.Since(start)) - }() - - response, err = handler(ctx, r, p, contentType) - if err != nil { - if writeErr := WriteErrorResponse(w, err.Error(), response.StatusCode); writeErr != nil { - log.WithError(writeErr).Error("Failed to write error response") - } - - return - } - - // Set headers before any potential content processing - for header, value := range response.Headers { - w.Header().Set(header, value) - } - - // Special case: Check for raw multipart content (stored as _raw_content in ExtraData) - if rawContent, ok := response.ExtraData["_raw_content"]; ok { - if rawBytes, ok := rawContent.([]byte); ok { - // Raw content handling - write directly to response - w.WriteHeader(response.StatusCode) - - if _, err := w.Write(rawBytes); err != nil { - log.WithError(err).Error("Failed to write raw content") - } - - return - } - } - - // Standard flow - marshal response based on content type - data, err := response.MarshalAs(contentType) - if err != nil { - if writeErr := WriteErrorResponse(w, err.Error(), http.StatusInternalServerError); writeErr != nil { - log.WithError(writeErr).Error("Failed to write error response") - } - - return - } - - if err := WriteContentAwareResponse(w, data, contentType); err != nil { - log.WithError(err).Error("Failed to write response") - } - } -} diff --git a/pkg/forky/api/metadata.go b/pkg/forky/api/metadata.go deleted file mode 100644 index 3ee713a..0000000 --- a/pkg/forky/api/metadata.go +++ /dev/null @@ -1,225 +0,0 @@ -package api - -import ( - "context" - "encoding/json" - "net/http" - - fhttp "github.com/ethpandaops/forky/pkg/forky/api/http" - "github.com/ethpandaops/forky/pkg/forky/service" - - "github.com/julienschmidt/httprouter" -) - -func (h *HTTP) handleV1MetadataList(ctx context.Context, r *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - // Grab our request body - var req fhttp.V1MetadataListRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fhttp.NewBadRequestResponse(nil), err - } - - filter := req.Filter - if filter == nil { - filter = &service.FrameFilter{} - } - - page := req.Pagination - if page == nil { - page = service.DefaultPagination() - } - - frames, pg, err := h.svc.ListMetadata(ctx, filter, *page) - if err != nil { - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1MetadataListResponse{ - Frames: frames, - Pagination: pg, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - return response, nil -} - -func (h *HTTP) handleV1MetadataListNodes(ctx context.Context, r *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - // Grab our request body - var req fhttp.V1MetadataListNodesRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fhttp.NewBadRequestResponse(nil), err - } - - page := req.Pagination - if page == nil { - page = service.DefaultPagination() - } - - filter := req.Filter - if filter == nil { - filter = &service.FrameFilter{} - } - - nodes, pg, err := h.svc.ListNodes(ctx, filter, *page) - if err != nil { - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1MetadataListNodesResponse{ - Nodes: nodes, - Pagination: pg, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - response.SetCacheControl("private, max-age=0, no-cache, no-store, must-revalidate") - - return response, nil -} - -func (h *HTTP) handleV1MetadataListSlots(ctx context.Context, r *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - // Grab our request body - var req fhttp.V1MetadataListSlotsRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fhttp.NewBadRequestResponse(nil), err - } - - page := req.Pagination - if page == nil { - page = service.DefaultPagination() - } - - filter := req.Filter - if filter == nil { - filter = &service.FrameFilter{} - } - - slots, pg, err := h.svc.ListSlots(ctx, filter, *page) - if err != nil { - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1MetadataListSlotsResponse{ - Slots: slots, - Pagination: pg, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - response.SetCacheControl("private, max-age=0, no-cache, no-store, must-revalidate") - - return response, nil -} - -func (h *HTTP) handleV1MetadataListEpochs(ctx context.Context, r *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - // Grab our request body - var req fhttp.V1MetadataListEpochsRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fhttp.NewBadRequestResponse(nil), err - } - - page := req.Pagination - if page == nil { - page = service.DefaultPagination() - } - - filter := req.Filter - if filter == nil { - filter = &service.FrameFilter{} - } - - epochs, pg, err := h.svc.ListEpochs(ctx, filter, *page) - if err != nil { - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1MetadataListEpochsResponse{ - Epochs: epochs, - Pagination: pg, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - response.SetCacheControl("private, max-age=0, no-cache, no-store, must-revalidate") - - return response, nil -} - -func (h *HTTP) handleV1MetadataListLabels(ctx context.Context, r *http.Request, p httprouter.Params, contentType fhttp.ContentType) (*fhttp.Response, error) { - if err := fhttp.ValidateContentType(contentType, []fhttp.ContentType{fhttp.ContentTypeJSON}); err != nil { - return fhttp.NewUnsupportedMediaTypeResponse(nil), err - } - - // Grab our request body - var req fhttp.V1MetadataListLabelsRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return fhttp.NewBadRequestResponse(nil), err - } - - page := req.Pagination - if page == nil { - page = service.DefaultPagination() - } - - filter := req.Filter - if filter == nil { - filter = &service.FrameFilter{} - } - - labels, pg, err := h.svc.ListLabels(ctx, filter, *page) - if err != nil { - return fhttp.NewInternalServerErrorResponse(nil), err - } - - rsp := fhttp.V1MetadataListLabelsResponse{ - Labels: labels, - Pagination: pg, - } - - response := fhttp.NewSuccessResponse(fhttp.ContentTypeResolvers{ - fhttp.ContentTypeJSON: func() ([]byte, error) { - return json.Marshal(rsp) - }, - }) - - response.SetCacheControl("private, max-age=0, no-cache, no-store, must-revalidate") - - return response, nil -} diff --git a/pkg/yaml/raw_config.go b/pkg/yaml/raw_config.go deleted file mode 100644 index 4c38572..0000000 --- a/pkg/yaml/raw_config.go +++ /dev/null @@ -1,15 +0,0 @@ -package yaml - -type RawMessage struct { - unmarshal func(interface{}) error -} - -func (r *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error { - r.unmarshal = unmarshal - - return nil -} - -func (r *RawMessage) Unmarshal(v interface{}) error { - return r.unmarshal(v) -} diff --git a/web/.gitignore b/web/.gitignore index 5b5d4ab..2654eae 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -10,6 +10,8 @@ lerna-debug.log* node_modules dist dist-ssr +storybook-static +.tanstack build *.local @@ -25,4 +27,4 @@ build *.sw? *.tsbuildinfo eslint_report.json -coverage \ No newline at end of file +coverage diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts new file mode 100644 index 0000000..c0d1bc6 --- /dev/null +++ b/web/.storybook/main.ts @@ -0,0 +1,23 @@ +import type { StorybookConfig } from '@storybook/tanstack-react'; + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@chromatic-com/storybook', + '@storybook/addon-docs', + '@storybook/addon-vitest', + '@storybook/addon-themes', + 'storybook/viewport', + ], + framework: { + name: '@storybook/tanstack-react', + options: {}, + }, + core: { + builder: '@storybook/builder-vite', + disableTelemetry: true, + }, + staticDirs: ['../public'], +}; + +export default config; diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx new file mode 100644 index 0000000..3bd2ce1 --- /dev/null +++ b/web/.storybook/preview.tsx @@ -0,0 +1,110 @@ +import type { Decorator, Preview } from '@storybook/tanstack-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en.json'; +import { initialize, mswLoader } from 'msw-storybook-addon'; +import { INITIAL_VIEWPORTS } from 'storybook/viewport'; + +import { storyRoute } from './story-routes'; +import '../src/index.css'; + +type StorybookThemeMode = 'light' | 'dark' | 'system'; + +TimeAgo.addLocale(en); +TimeAgo.setDefaultLocale('en'); + +initialize({ + onUnhandledRequest: 'bypass', + quiet: true, +}); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: Infinity, + }, + }, +}); + +function applyThemeMode(themeMode: StorybookThemeMode) { + if (typeof window === 'undefined') return; + + if (themeMode === 'system') { + delete window.localStorage.isDarkMode; + document.documentElement.classList.toggle( + 'dark', + window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false, + ); + return; + } + + const isDarkMode = themeMode === 'dark'; + window.localStorage.isDarkMode = String(isDarkMode); + document.documentElement.classList.toggle('dark', isDarkMode); +} + +const decorators: Decorator[] = [ + (Story, context) => { + const themeMode = (context.globals.themeMode as StorybookThemeMode | undefined) ?? 'system'; + applyThemeMode(themeMode); + + return ( + + + + ); + }, +]; + +const preview: Preview = { + globalTypes: { + themeMode: { + name: 'Theme', + description: 'Global theme mode', + defaultValue: 'system', + toolbar: { + icon: 'mirror', + dynamicTitle: true, + items: [ + { value: 'light', title: 'Light' }, + { value: 'dark', title: 'Dark' }, + { value: 'system', title: 'System' }, + ], + }, + }, + }, + beforeEach: () => { + queryClient.clear(); + }, + loaders: [mswLoader], + decorators, + parameters: { + tanstack: { + router: { + route: storyRoute, + path: '/', + context: { queryClient }, + }, + }, + msw: { + handlers: [], + }, + viewport: { + viewports: INITIAL_VIEWPORTS, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + options: { + storySort: { + method: 'alphabetical', + }, + }, + }, +}; + +export default preview; diff --git a/web/.storybook/story-routes.tsx b/web/.storybook/story-routes.tsx new file mode 100644 index 0000000..4e25da6 --- /dev/null +++ b/web/.storybook/story-routes.tsx @@ -0,0 +1,10 @@ +import { createRootRoute, createRoute } from '@tanstack/react-router'; + +const rootRoute = createRootRoute(); + +export const storyRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '$', +}); + +rootRoute.addChildren([storyRoute]); diff --git a/web/.storybook/vitest-setup.ts b/web/.storybook/vitest-setup.ts new file mode 100644 index 0000000..a312078 --- /dev/null +++ b/web/.storybook/vitest-setup.ts @@ -0,0 +1,22 @@ +import '@testing-library/jest-dom'; +import { beforeAll, vi } from 'vitest'; + +beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'clientWidth', { + configurable: true, + value: 800, + }); + + Object.defineProperty(HTMLElement.prototype, 'clientHeight', { + configurable: true, + value: 600, + }); + + const ResizeObserverMock = vi.fn(function (this: ResizeObserver) { + this.observe = vi.fn(); + this.unobserve = vi.fn(); + this.disconnect = vi.fn(); + }); + + vi.stubGlobal('ResizeObserver', ResizeObserverMock); +}); diff --git a/web/CLAUDE.md b/web/CLAUDE.md new file mode 100644 index 0000000..01b2bd2 --- /dev/null +++ b/web/CLAUDE.md @@ -0,0 +1,101 @@ +# forky web + +The operator web UI for forky — a React single-page app that visualizes +Ethereum Beacon Chain fork-choice data. Built with Vite and embedded into the +forky binary (see the repo-root `CLAUDE.md`). + +## Commands + +pnpm is the package manager. + +```bash +pnpm install +pnpm dev # Vite dev server (proxies /api to a backend; see vite.config.ts) +pnpm dev:mock # dev server with MSW mocks (VITE_MOCK=true) +pnpm build # type-check + build production assets into dist/ (embedded by the Go binary) +pnpm preview # preview the production build +pnpm lint # eslint +pnpm typecheck # tsc --noEmit +pnpm format # prettier --write src +pnpm test # vitest +pnpm generate:api # regenerate the typed API client from ../api/openapi.yaml +``` + +## Stack + +- pnpm, Vite 8, React 19, TypeScript 6 +- @tanstack/react-router (file-based routing), @tanstack/react-query +- Tailwind CSS v4 (via @tailwindcss/vite) +- graphology (graph data structures + layout) for the fork-choice visualization +- MSW for API mocking in dev/tests; Vitest for tests +- @hey-api/openapi-ts + zod for the generated, typed API client + +## API client + +The OpenAPI spec at `../api/openapi.yaml` is the source of truth — the Go +server is generated from the same spec (ogen), so the wire format matches by +construction and SDK responses are validated against the zod schemas at +runtime. Run `pnpm generate:api` (or `make generate` at the repo root) to +regenerate the typed client, TanStack Query helpers and zod schemas into +`src/api/` (do not edit by hand — the whole directory is generated). Runtime +client config (base URL, fetch) lives in `src/utils/api-config.ts`. MSW mocks +in `src/mocks/` are typed against the generated response types, so tests fail +if the mocks drift from the spec. + +Domain hooks in `src/hooks/useQuery.ts` wrap the generated SDK and adapt +responses to the app's view types — components consume those hooks, not the +generated client directly. + +## Routing + +TanStack Router with file-based routes. `src/routes/` holds the route +definitions (`__root.tsx`, `index.tsx`, `byo.tsx`, `events.tsx`, `node.$.tsx`, +`snapshot.$frameId.tsx` and the non-nested `_.events` variants); +`routeTree.gen.ts` is generated by the Vite plugin. Forky's URL scheme models +top-level views (`/byo`, `/node/`, `/snapshot/`) and route-specific +event overlays (`/events`, `/byo/events`, `/node//events`, +`/snapshot//events`). Node ids contain literal slashes +(`ethpandaops/mainnet/node-001`), so `/node` is a splat route (`node.$.tsx`) +whose remainder is the node id plus an optional trailing `/events` overlay +segment (parsed by `parseNodeSplat` in `src/utils/routes.ts`). + +The root route renders the single, persistent `` for every view and +derives view props from the location via `parseAppRoute`; the leaf route +files are matchers only (they render nothing). This keeps one App — and the +focus provider that owns the player's focused time and play/pause state — +mounted across navigation, so switching between aggregated, node and +snapshot views never resets playback. Don't add `component`s to leaf routes; +add new views to the `parseAppRoute` switch in `__root.tsx` instead. +`src/hooks/useAppNavigation.ts` (`usePathname`, `useAppNavigate`) and +`src/components/Link.tsx` remain the app's routing primitives for components +that pass plain path strings. + +## Structure + +``` +src/ + api/ # generated, typed API client (do not edit) + routes/ # TanStack Router route definitions + routeTree.gen.ts + components/ # React components (graph nodes, summaries, controls, ...) + contexts/ # React contexts (ethereum, focus, selection) + providers/ # Context providers + hooks/ # Custom hooks (useQuery, useGraph, useActive, useLocation, ...) + parts/ # Page sections (Graph, Header, Timeline, Stage, ...) + utils/ # Helpers (graph, maths, strings, api-config, ...) + types/ # Shared TypeScript view types + index.css # CSS entry: Tailwind + two-tier color tokens (see Theming) + mocks/ # MSW handlers + index.tsx # Entry point (QueryClientProvider + RouterProvider) +``` + +## Conventions + +- Path aliases (`@app/*`, `@api`, `@components/*`, `@hooks/*`, ...) — see + `tsconfig.app.json`. Prefer aliases over deep relative imports. +- Theming: colors follow a two-tier token architecture in `src/index.css` — + Tier 1 primitive values feed Tier 2 semantic tokens (`bg-surface`, + `text-foreground`, `text-canonical`, ...). Application code may only use + the semantic tokens and never writes `dark:` variants for colors (light and + dark values flip via CSS variables on `html.dark`). Enforced by the custom + ESLint rules in `eslint-rules/`. +- Run `pnpm lint` and `pnpm build` before committing. diff --git a/web/build/.gitkeep b/web/build/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..f8462a9 --- /dev/null +++ b/web/embed.go @@ -0,0 +1,17 @@ +//go:build webui + +// Package web embeds the frontend build output from web/dist/. +package web + +import ( + "embed" + "io/fs" +) + +//go:embed all:dist/* +var embeddedFiles embed.FS + +// GetFS returns the embedded dist filesystem rooted at dist/. +func GetFS() (fs.FS, error) { + return fs.Sub(embeddedFiles, "dist") +} diff --git a/web/embed_stub.go b/web/embed_stub.go new file mode 100644 index 0000000..e2f7589 --- /dev/null +++ b/web/embed_stub.go @@ -0,0 +1,18 @@ +//go:build !webui + +// Package web provides the frontend filesystem. When built without the +// webui tag, GetFS returns an empty filesystem. +package web + +import "io/fs" + +type emptyFS struct{} + +func (emptyFS) Open(string) (fs.File, error) { + return nil, &fs.PathError{Op: "open", Path: "/", Err: fs.ErrNotExist} +} + +// GetFS returns an empty filesystem when the webui build tag is not set. +func GetFS() (fs.FS, error) { + return emptyFS{}, nil +} diff --git a/web/eslint-rules/index.cjs b/web/eslint-rules/index.cjs new file mode 100644 index 0000000..3ad1619 --- /dev/null +++ b/web/eslint-rules/index.cjs @@ -0,0 +1,14 @@ +/** + * @fileoverview Custom ESLint rules for the forky frontend + * + * These rules enforce the two-tier color architecture: all colors should be + * defined in src/index.css as primitive scales + semantic tokens, and + * application code should only reference the semantic tokens. + */ + +module.exports = { + rules: { + 'no-hardcoded-colors': require('./no-hardcoded-colors.cjs'), + 'no-primitive-color-scales': require('./no-primitive-color-scales.cjs'), + }, +}; diff --git a/web/eslint-rules/no-hardcoded-colors.cjs b/web/eslint-rules/no-hardcoded-colors.cjs new file mode 100644 index 0000000..1831f6a --- /dev/null +++ b/web/eslint-rules/no-hardcoded-colors.cjs @@ -0,0 +1,263 @@ +/** + * @fileoverview Ban hardcoded colors (hex, rgb, hsl) in Tailwind classes and inline styles + * + * This rule prevents developers from using hardcoded color values in Tailwind + * className strings and React inline styles. All colors should come from the + * semantic color tokens defined in src/index.css. + * + * Examples of incorrect code: + * className="bg-[#ff0000]" + * className="text-[rgb(255,0,0)]" + * className="border-[hsl(0,100%,50%)]" + * style={{ color: '#ff0000' }} + * style={{ backgroundColor: 'rgb(255,0,0)' }} + * + * Examples of correct code: + * className="bg-primary" + * className="text-danger" + * className="border-accent" + * style={{ color: 'var(--color-primary)' }} + */ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Disallow hardcoded colors in Tailwind classes', + category: 'Best Practices', + recommended: true, + }, + messages: { + hardcodedHexColor: + 'Hardcoded hex color "{{value}}" detected. Use semantic color tokens from src/index.css instead (e.g., bg-primary, var(--color-primary)).', + hardcodedRgbColor: + 'Hardcoded RGB color "{{value}}" detected. Use semantic color tokens from src/index.css instead (e.g., bg-primary, var(--color-primary)).', + hardcodedHslColor: + 'Hardcoded HSL color "{{value}}" detected. Use semantic color tokens from src/index.css instead (e.g., bg-primary, var(--color-primary)).', + hardcodedNamedColor: + 'Hardcoded named color "{{value}}" detected in {{property}}. Use semantic color tokens from src/index.css instead (e.g., var(--color-primary)).', + }, + schema: [], + }, + + create(context) { + // Patterns to detect hardcoded colors in Tailwind arbitrary values + const hexPattern = /\[#[0-9a-fA-F]{3,8}\]/g; + const rgbPattern = /\[rgba?\([^)]+\)\]/g; + const hslPattern = /\[hsla?\([^)]+\)\]/g; + + // Patterns for inline style values (without Tailwind brackets) + const hexValuePattern = /^#[0-9a-fA-F]{3,8}$/; + const rgbValuePattern = /^rgba?\([^)]+\)$/; + const hslValuePattern = /^hsla?\([^)]+\)$/; + + // Common named colors to ban + const namedColors = new Set([ + 'black', + 'white', + 'red', + 'green', + 'blue', + 'yellow', + 'orange', + 'purple', + 'pink', + 'gray', + 'grey', + 'brown', + 'cyan', + 'magenta', + 'lime', + 'indigo', + 'violet', + 'gold', + 'silver', + 'navy', + 'teal', + 'olive', + 'maroon', + 'aqua', + 'fuchsia', + ]); + + // Style properties that accept color values + const colorProperties = new Set([ + 'color', + 'backgroundColor', + 'borderColor', + 'borderTopColor', + 'borderRightColor', + 'borderBottomColor', + 'borderLeftColor', + 'outlineColor', + 'textDecorationColor', + 'fill', + 'stroke', + 'caretColor', + 'accentColor', + ]); + + /** + * Check if a string value contains hardcoded color values + * @param {import('estree').Node} node - The AST node + * @param {string} value - The string value to check + */ + function checkForHardcodedColors(node, value) { + // Check for hex colors + const hexMatches = value.match(hexPattern); + if (hexMatches) { + hexMatches.forEach(match => { + context.report({ + node, + messageId: 'hardcodedHexColor', + data: { value: match }, + }); + }); + } + + // Check for RGB colors + const rgbMatches = value.match(rgbPattern); + if (rgbMatches) { + rgbMatches.forEach(match => { + context.report({ + node, + messageId: 'hardcodedRgbColor', + data: { value: match }, + }); + }); + } + + // Check for HSL colors + const hslMatches = value.match(hslPattern); + if (hslMatches) { + hslMatches.forEach(match => { + context.report({ + node, + messageId: 'hardcodedHslColor', + data: { value: match }, + }); + }); + } + } + + /** + * Check if a style object property has a hardcoded color value + * @param {import('estree').Node} node - The property node + * @param {string} propertyName - The property name + * @param {*} valueNode - The value node + */ + function checkStyleProperty(node, propertyName, valueNode) { + if (!colorProperties.has(propertyName)) { + return; + } + + // Handle string literal values + if (valueNode.type === 'Literal' && typeof valueNode.value === 'string') { + const value = valueNode.value; + + // Check for hex colors + if (hexValuePattern.test(value)) { + context.report({ + node: valueNode, + messageId: 'hardcodedHexColor', + data: { value }, + }); + return; + } + + // Check for RGB colors + if (rgbValuePattern.test(value)) { + context.report({ + node: valueNode, + messageId: 'hardcodedRgbColor', + data: { value }, + }); + return; + } + + // Check for HSL colors + if (hslValuePattern.test(value)) { + context.report({ + node: valueNode, + messageId: 'hardcodedHslColor', + data: { value }, + }); + return; + } + + // Check for named colors + if ( + namedColors.has(value.toLowerCase()) && + !value.startsWith('var(') && + value !== 'currentColor' && + value !== 'transparent' && + value !== 'inherit' + ) { + context.report({ + node: valueNode, + messageId: 'hardcodedNamedColor', + data: { value, property: propertyName }, + }); + } + } + } + + return { + // Handle JSX className attributes + JSXAttribute(node) { + if ( + node.name.name === 'className' && + node.value && + node.value.type === 'Literal' && + typeof node.value.value === 'string' + ) { + checkForHardcodedColors(node, node.value.value); + } + + // Handle template literals in className + if ( + node.name.name === 'className' && + node.value && + node.value.type === 'JSXExpressionContainer' && + node.value.expression.type === 'TemplateLiteral' + ) { + node.value.expression.quasis.forEach(quasi => { + checkForHardcodedColors(node, quasi.value.raw); + }); + } + + // Handle style attribute with object expression + if ( + node.name.name === 'style' && + node.value && + node.value.type === 'JSXExpressionContainer' && + node.value.expression.type === 'ObjectExpression' + ) { + node.value.expression.properties.forEach(prop => { + if (prop.type === 'Property' && prop.key.type === 'Identifier') { + checkStyleProperty(node, prop.key.name, prop.value); + } + }); + } + }, + + // Handle clsx/classnames/cn function calls + CallExpression(node) { + const functionName = node.callee.name; + if (['clsx', 'classnames', 'cn', 'cva'].includes(functionName)) { + node.arguments.forEach(arg => { + if (arg.type === 'Literal' && typeof arg.value === 'string') { + checkForHardcodedColors(arg, arg.value); + } + if (arg.type === 'TemplateLiteral') { + arg.quasis.forEach(quasi => { + checkForHardcodedColors(arg, quasi.value.raw); + }); + } + }); + } + }, + }; + }, +}; diff --git a/web/eslint-rules/no-primitive-color-scales.cjs b/web/eslint-rules/no-primitive-color-scales.cjs new file mode 100644 index 0000000..c6e8fa2 --- /dev/null +++ b/web/eslint-rules/no-primitive-color-scales.cjs @@ -0,0 +1,119 @@ +/** + * @fileoverview Ban primitive color scales in Tailwind classes + * + * This rule prevents developers from using primitive color scales directly + * (Tailwind palette scales like blue-*, red-*, neutral-*) in Tailwind className strings. + * All UI colors should use semantic tokens defined in src/index.css. + * + * Primitive scales are Tier 1 foundation colors and should only be referenced + * in the theme definition (src/index.css). Application code should use Tier 2 + * semantic tokens. + * + * Examples of incorrect code: + * className="bg-neutral-700" + * className="text-neutral-500" + * className="border-neutral-300" + * + * Examples of correct code: + * className="bg-primary" + * className="text-foreground" + * className="border-border" + * className="bg-accent" + */ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Disallow primitive color scales in Tailwind classes', + category: 'Best Practices', + recommended: true, + }, + messages: { + primitiveColorScale: + 'Primitive color scale "{{scale}}" detected in "{{match}}". Use semantic tokens instead:\n' + + ' • Brand: primary, secondary, accent\n' + + ' • Surface: background, surface, foreground, muted, border\n' + + ' • State: success, warning, danger', + }, + schema: [], + }, + + create(context) { + // Tailwind palette scales that should NOT be used directly in app code + const primitiveScales = [ + 'slate', + 'gray', + 'zinc', + 'neutral', + 'stone', + 'red', + 'orange', + 'amber', + 'yellow', + 'lime', + 'green', + 'emerald', + 'teal', + 'cyan', + 'sky', + 'blue', + 'indigo', + 'violet', + 'purple', + 'fuchsia', + 'pink', + 'rose', + 'white', + 'black', + + ]; + + // Build regex pattern to match any Tailwind class using primitive scales + // Matches patterns like: bg-neutral-700, text-neutral-500, border-neutral-300 + const primitivePattern = new RegExp( + `\\b(?:bg|text|border(?:-[xytrbls]|-[se])?|from|via|to|ring|outline|decoration|divide(?:-[xy])?|accent|caret|fill|stroke|shadow)-(?:${primitiveScales.join('|')})(?:-(?:50|100|200|300|400|500|600|700|800|900|950))?(?:\\/\\d+)?\\b`, + 'g' + ); + + /** + * Check if a string value contains primitive color scale usage + * @param {import('estree').Node} node - The AST node + * @param {string} value - The string value to check + */ + function checkForPrimitiveColors(node, value) { + const matches = value.match(primitivePattern); + if (matches) { + matches.forEach(match => { + // Extract the scale name from the match + const scaleMatch = primitiveScales.find(scale => match.includes(scale)); + context.report({ + node, + messageId: 'primitiveColorScale', + data: { + scale: scaleMatch, + match: match, + }, + }); + }); + } + } + + return { + // Scan every string literal: catches className attributes, ternaries, + // arrays, props and helper-function arguments alike. + Literal(node) { + if (typeof node.value === 'string') { + checkForPrimitiveColors(node, node.value); + } + }, + + TemplateLiteral(node) { + node.quasis.forEach(quasi => { + checkForPrimitiveColors(node, quasi.value.raw); + }); + }, + }; + }, +}; diff --git a/web/eslint.config.js b/web/eslint.config.js index 243d292..c1466da 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -1,3 +1,6 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from 'eslint-plugin-storybook'; + import js from '@eslint/js'; import globals from 'globals'; import reactHooks from 'eslint-plugin-react-hooks'; @@ -5,19 +8,35 @@ import reactRefresh from 'eslint-plugin-react-refresh'; import tseslint from 'typescript-eslint'; import prettier from 'eslint-plugin-prettier/recommended'; import vitest from '@vitest/eslint-plugin'; +import customRules from './eslint-rules/index.cjs'; export default tseslint.config( { ignores: [ 'build', + 'dist', 'public', - 'tailwind.config.js', - 'craco.config.js', + 'src/api', + 'src/routeTree.gen.ts', 'node_modules', 'coverage', - 'eslint_report.json' + 'eslint_report.json', + 'eslint-rules', + 'storybook-static', ], }, + // Custom color theming rules: app code may only use the semantic tokens + // defined in src/index.css. + { + files: ['**/*.{ts,tsx}'], + plugins: { + forky: customRules, + }, + rules: { + 'forky/no-hardcoded-colors': 'error', + 'forky/no-primitive-color-scales': 'error', + }, + }, { extends: [js.configs.recommended, ...tseslint.configs.recommended, vitest.configs.recommended, prettier], files: ['**/*.{ts,tsx}'], @@ -35,6 +54,11 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, + // React Compiler readiness rules (react-hooks v7). The codebase is + // clean; keep them as errors so regressions fail lint. + 'react-hooks/purity': 'error', + 'react-hooks/set-state-in-effect': 'error', + 'react-hooks/immutability': 'error', 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'react/prop-types': 'off', 'prettier/prettier': [ @@ -46,10 +70,25 @@ export default tseslint.config( proseWrap: 'never', }, ], - radix: ['error', 'as-needed'], + // The codebase intentionally parses base-10 decimal strings (slots, + // epochs) without an explicit radix. + radix: 'off', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off', 'react/react-in-jsx-scope': 'off', }, }, + storybook.configs['flat/recommended'], + { + files: ['**/.storybook/**/*.{js,ts}'], + rules: { + 'storybook/no-uninstalled-addons': [ + 'error', + { + packageJsonLocation: './package.json', + ignore: ['storybook/viewport'], + }, + ], + }, + } ); diff --git a/web/index.html b/web/index.html index 9902a30..97b984d 100644 --- a/web/index.html +++ b/web/index.html @@ -8,9 +8,9 @@ - - - + + + Forky: Ethereum Beacon Chain Fork Choice Data Visualization & Analysis Tool diff --git a/web/openapi-ts.config.ts b/web/openapi-ts.config.ts new file mode 100644 index 0000000..4f718b2 --- /dev/null +++ b/web/openapi-ts.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from '@hey-api/openapi-ts'; + +const OPENAPI_INPUT = process.env.OPENAPI_INPUT || '../api/openapi.yaml'; + +export default defineConfig({ + input: OPENAPI_INPUT, + output: { + path: 'src/api', + postProcess: ['prettier'], + }, + plugins: [ + { + name: '@hey-api/client-fetch', + runtimeConfigPath: './src/utils/api-config.ts', + }, + { + name: 'zod', + compatibilityVersion: 'mini', + metadata: false, + }, + '@tanstack/react-query', + // The backend is generated from the same spec (ogen), so responses are + // validated against the zod schemas at runtime. + { + name: '@hey-api/sdk', + validator: { + request: false, + response: true, + }, + }, + ], +}); diff --git a/web/package-lock.json b/web/package-lock.json deleted file mode 100644 index 5bde9da..0000000 --- a/web/package-lock.json +++ /dev/null @@ -1,7025 +0,0 @@ -{ - "name": "forkchoice", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "forkchoice", - "version": "0.0.0", - "dependencies": { - "@headlessui/react": "2.2.2", - "@heroicons/react": "2.2.0", - "@tailwindcss/vite": "4.1.5", - "@tanstack/react-query": "5.75.0", - "@tanstack/react-query-devtools": "5.75.0", - "@types/pako": "2.0.3", - "clsx": "2.1.1", - "graphology": "0.26.0", - "graphology-layout": "0.6.1", - "graphology-traversal": "0.3.1", - "graphology-types": "0.24.8", - "graphology-utils": "2.5.2", - "javascript-time-ago": "2.5.11", - "pako": "2.1.0", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-router-dom": "7.5.3", - "react-time-ago": "7.3.3", - "react-zoom-pan-pinch": "3.7.0", - "wouter": "3.7.0" - }, - "devDependencies": { - "@eslint/js": "9.25.1", - "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.3.0", - "@types/js-yaml": "4.0.9", - "@types/react": "19.1.2", - "@types/react-dom": "19.1.3", - "@vitejs/plugin-react": "4.4.1", - "@vitest/coverage-v8": "3.1.2", - "@vitest/eslint-plugin": "1.1.44", - "autoprefixer": "10.4.21", - "eslint": "9.25.1", - "eslint-config-prettier": "10.1.2", - "eslint-plugin-prettier": "5.2.6", - "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-react-refresh": "0.4.20", - "globals": "16.0.0", - "jsdom": "26.1.0", - "msw": "2.7.5", - "prettier": "3.5.3", - "prop-types": "15.8.1", - "typescript": "5.8.3", - "typescript-eslint": "8.31.1", - "vite": "6.3.4", - "vite-tsconfig-paths": "5.1.4", - "vitest": "3.1.2", - "vitest-github-actions-reporter": "0.11.1" - } - }, - "node_modules/@actions/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/exec": "^1.1.1", - "@actions/http-client": "^2.0.1" - } - }, - "node_modules/@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/io": "^1.0.1" - } - }, - "node_modules/@actions/http-client": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", - "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "^0.0.6", - "undici": "^5.25.4" - } - }, - "node_modules/@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.5.tgz", - "integrity": "sha512-w7AmVyTTiU41fNLsFDf+gA2Dwtbx2EJtn2pbJNAGSRAg50loXy1uLXA3hEpD8+eydcomTurw09tq5/AyceCaGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", - "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cookie": "^0.7.2" - } - }, - "node_modules/@bundled-es-modules/statuses": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", - "dev": true, - "license": "ISC", - "dependencies": { - "statuses": "^2.0.1" - } - }, - "node_modules/@bundled-es-modules/tough-cookie": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", - "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@types/tough-cookie": "^4.0.5", - "tough-cookie": "^4.1.4" - } - }, - "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz", - "integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz", - "integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", - "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, - "node_modules/@headlessui/react": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.2.tgz", - "integrity": "sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.17.1", - "@react-aria/interactions": "^3.21.3", - "@tanstack/react-virtual": "^3.13.6", - "use-sync-external-store": "^1.5.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", - "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", - "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", - "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mswjs/interceptors": { - "version": "0.37.6", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", - "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@react-aria/focus": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", - "integrity": "sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/interactions": "^3.25.0", - "@react-aria/utils": "^3.28.2", - "@react-types/shared": "^3.29.0", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/interactions": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.0.tgz", - "integrity": "sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.8", - "@react-aria/utils": "^3.28.2", - "@react-stately/flags": "^3.1.1", - "@react-types/shared": "^3.29.0", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.8", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz", - "integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.28.2", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.2.tgz", - "integrity": "sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.8", - "@react-stately/flags": "^3.1.1", - "@react-stately/utils": "^3.10.6", - "@react-types/shared": "^3.29.0", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-stately/flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz", - "integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.6", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz", - "integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-types/shared": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.0.tgz", - "integrity": "sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.5.tgz", - "integrity": "sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==", - "license": "MIT", - "dependencies": { - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.29.2", - "tailwindcss": "4.1.5" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.5.tgz", - "integrity": "sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==", - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.5", - "@tailwindcss/oxide-darwin-arm64": "4.1.5", - "@tailwindcss/oxide-darwin-x64": "4.1.5", - "@tailwindcss/oxide-freebsd-x64": "4.1.5", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.5", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.5", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.5", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.5", - "@tailwindcss/oxide-linux-x64-musl": "4.1.5", - "@tailwindcss/oxide-wasm32-wasi": "4.1.5", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.5", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.5" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.5.tgz", - "integrity": "sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.5.tgz", - "integrity": "sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.5.tgz", - "integrity": "sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.5.tgz", - "integrity": "sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.5.tgz", - "integrity": "sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.5.tgz", - "integrity": "sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.5.tgz", - "integrity": "sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.5.tgz", - "integrity": "sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.5.tgz", - "integrity": "sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.5.tgz", - "integrity": "sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.5.tgz", - "integrity": "sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.5.tgz", - "integrity": "sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.5.tgz", - "integrity": "sha512-FE1stRoqdHSb7RxesMfCXE8icwI1W6zGE/512ae3ZDrpkQYTTYeSyUJPRCjZd8CwVAhpDUbi1YR8pcZioFJQ/w==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.5", - "@tailwindcss/oxide": "4.1.5", - "tailwindcss": "4.1.5" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.0.tgz", - "integrity": "sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-devtools": { - "version": "5.74.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz", - "integrity": "sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.0.tgz", - "integrity": "sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.75.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-query-devtools": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.75.0.tgz", - "integrity": "sha512-ogTsCCONHPq8opyeU/NJZlus8+g36KnK+cybC1cs0Knr8iO/wAYgnciz+3DVNq38kRflBb0iJhHkUexp/7WfcQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-devtools": "5.74.7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-query": "^5.75.0", - "react": "^18 || ^19" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz", - "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.6", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz", - "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/pako": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz", - "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.1.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", - "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", - "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@types/statuses": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", - "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", - "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", - "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", - "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", - "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", - "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", - "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", - "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", - "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.31.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.2.tgz", - "integrity": "sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "debug": "^4.4.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.1.2", - "vitest": "3.1.2" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/eslint-plugin": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.44.tgz", - "integrity": "sha512-m4XeohMT+Dj2RZfxnbiFR+Cv5dEC0H7C6TlxRQT7GK2556solm99kxgzJp/trKrZvanZcOFyw7aABykUTfWyrg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@typescript-eslint/utils": ">= 8.24.0", - "eslint": ">= 8.57.0", - "typescript": ">= 5.0.0", - "vitest": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz", - "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz", - "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.1.2", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz", - "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.1.2", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz", - "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.1.2", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz", - "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz", - "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.1.2", - "loupe": "^3.1.3", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001716", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssstyle": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz", - "integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.1.2", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.149", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.149.tgz", - "integrity": "sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.25.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", - "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.25.1", - "@eslint/plugin-kit": "^0.2.8", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", - "integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", - "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", - "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/graphology": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz", - "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0" - }, - "peerDependencies": { - "graphology-types": ">=0.24.0" - } - }, - "node_modules/graphology-indices": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz", - "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==", - "license": "MIT", - "dependencies": { - "graphology-utils": "^2.4.2", - "mnemonist": "^0.39.0" - }, - "peerDependencies": { - "graphology-types": ">=0.20.0" - } - }, - "node_modules/graphology-layout": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/graphology-layout/-/graphology-layout-0.6.1.tgz", - "integrity": "sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==", - "license": "MIT", - "dependencies": { - "graphology-utils": "^2.3.0", - "pandemonium": "^2.4.0" - }, - "peerDependencies": { - "graphology-types": ">=0.19.0" - } - }, - "node_modules/graphology-traversal": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/graphology-traversal/-/graphology-traversal-0.3.1.tgz", - "integrity": "sha512-lGLrLKEDKtNgAKgHVhVftKf3cb/nuWwuVPQZHXRnN90JWn0RSjco/s+NB2ARSlMapEMlbnPgv6j++427yTnU3Q==", - "license": "MIT", - "dependencies": { - "graphology-indices": "^0.17.0", - "graphology-utils": "^2.0.0" - }, - "peerDependencies": { - "graphology-types": ">=0.20.0" - } - }, - "node_modules/graphology-types": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", - "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", - "license": "MIT" - }, - "node_modules/graphology-utils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", - "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", - "license": "MIT", - "peerDependencies": { - "graphology-types": ">=0.23.0" - } - }, - "node_modules/graphql": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", - "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/javascript-time-ago": { - "version": "2.5.11", - "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.11.tgz", - "integrity": "sha512-Zeyf5R7oM1fSMW9zsU3YgAYwE0bimEeF54Udn2ixGd8PUwu+z1Yc5t4Y8YScJDMHD6uCx6giLt3VJR5K4CMwbg==", - "license": "MIT", - "dependencies": { - "relative-time-format": "^1.1.6" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.1" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/msw": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.5.tgz", - "integrity": "sha512-00MyTlY3TJutBa5kiU+jWiz2z5pNJDYHn2TgPkGkh92kMmNH43RqvMXd8y/7HxNn8RjzUbvZWYZjcS36fdb6sw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@bundled-es-modules/cookie": "^2.0.1", - "@bundled-es-modules/statuses": "^1.0.1", - "@bundled-es-modules/tough-cookie": "^0.1.6", - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.37.0", - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/until": "^2.1.0", - "@types/cookie": "^0.6.0", - "@types/statuses": "^2.0.4", - "graphql": "^16.8.1", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "strict-event-emitter": "^0.5.1", - "type-fest": "^4.26.1", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/obliterator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" - }, - "node_modules/pandemonium": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz", - "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==", - "license": "MIT", - "dependencies": { - "mnemonist": "^0.39.2" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "license": "MIT", - "dependencies": { - "performance-now": "^2.1.0" - } - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", - "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0", - "turbo-stream": "2.4.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz", - "integrity": "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==", - "license": "MIT", - "dependencies": { - "react-router": "7.5.3" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/react-time-ago": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.3.3.tgz", - "integrity": "sha512-5kh2Kuu/UhHzcZrGvf3GUrF2d+IXjkIXif5MR2iDWIfSqQuBW27/ejN/tmzJBRyPiryYTgbDIG6AZFJ4RW3yfw==", - "license": "MIT", - "dependencies": { - "memoize-one": "^6.0.0", - "prop-types": "^15.8.1", - "raf": "^3.4.1" - }, - "peerDependencies": { - "javascript-time-ago": "^2.3.7", - "react": ">=0.16.8", - "react-dom": ">=0.16.8" - } - }, - "node_modules/react-zoom-pan-pinch": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", - "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", - "license": "MIT", - "engines": { - "node": ">=8", - "npm": ">=5" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/regexparam": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", - "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/relative-time-format": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", - "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==", - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.1", - "@rollup/rollup-android-arm64": "4.40.1", - "@rollup/rollup-darwin-arm64": "4.40.1", - "@rollup/rollup-darwin-x64": "4.40.1", - "@rollup/rollup-freebsd-arm64": "4.40.1", - "@rollup/rollup-freebsd-x64": "4.40.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", - "@rollup/rollup-linux-arm64-gnu": "4.40.1", - "@rollup/rollup-linux-arm64-musl": "4.40.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", - "@rollup/rollup-linux-riscv64-musl": "4.40.1", - "@rollup/rollup-linux-s390x-gnu": "4.40.1", - "@rollup/rollup-linux-x64-gnu": "4.40.1", - "@rollup/rollup-linux-x64-musl": "4.40.1", - "@rollup/rollup-win32-arm64-msvc": "4.40.1", - "@rollup/rollup-win32-ia32-msvc": "4.40.1", - "@rollup/rollup-win32-x64-msvc": "4.40.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "dev": true, - "license": "MIT" - }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "license": "MIT" - }, - "node_modules/tailwindcss": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.5.tgz", - "integrity": "sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsconfck": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", - "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", - "license": "ISC" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz", - "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", - "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.31.1", - "@typescript-eslint/parser": "8.31.1", - "@typescript-eslint/utils": "8.31.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz", - "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.0", - "es-module-lexer": "^1.6.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-tsconfig-paths": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", - "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz", - "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "3.1.2", - "@vitest/mocker": "3.1.2", - "@vitest/pretty-format": "^3.1.2", - "@vitest/runner": "3.1.2", - "@vitest/snapshot": "3.1.2", - "@vitest/spy": "3.1.2", - "@vitest/utils": "3.1.2", - "chai": "^5.2.0", - "debug": "^4.4.0", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.2", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.2", - "@vitest/ui": "3.1.2", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest-github-actions-reporter": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/vitest-github-actions-reporter/-/vitest-github-actions-reporter-0.11.1.tgz", - "integrity": "sha512-ZHHB0wBgOPhMYCB17WKVlJZa+5SdudBZFoVoebwfq3ioIUTeLQGYHwh85vpdJAxRghLP8d0qI/6eCTueGyDVXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/core": "^1.10.0" - }, - "engines": { - "node": ">=14.16.0" - }, - "peerDependencies": { - "vitest": ">=0.28.5" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wouter": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.7.0.tgz", - "integrity": "sha512-ElkqESy10WgRRbdd22uQkSSO6LmG6vxI50pxPKry2TLebbdXvgtp7rJ48KafjUvsKcQQmIewxgRCGixPfk58tw==", - "license": "Unlicense", - "dependencies": { - "mitt": "^3.0.1", - "regexparam": "^3.0.0", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/web/package.json b/web/package.json index 95183e3..4bde556 100644 --- a/web/package.json +++ b/web/package.json @@ -3,70 +3,106 @@ "private": true, "version": "0.0.0", "type": "module", + "packageManager": "pnpm@10.30.1", "scripts": { "build": "tsc -b tsconfig.app.json && vite build", "dev:mock": "VITE_MOCK=true vite", "dev": "vite", "generate": "NODE_OPTIONS=--experimental-strip-types NODE_NO_WARNINGS=1 node dataset-generate.ts", + "generate:api": "openapi-ts", "lint:report": "eslint --output-file eslint_report.json --format json src", "lint": "eslint .", "preview": "vite preview", + "typecheck": "tsc --noEmit", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", "test:coverage": "vitest run --coverage", - "test": "vitest" + "test": "pnpm test:unit && pnpm test:storybook", + "test:unit": "vitest run --no-ui --config vitest.config.unit.ts --passWithNoTests", + "test:unit:watch": "vitest --no-ui --config vitest.config.unit.ts", + "test:storybook": "vitest run --no-ui --config vitest.config.ts", + "test:storybook:watch": "vitest --no-ui --config vitest.config.ts", + "test:ui": "vitest --ui", + "storybook": "storybook dev -p 6006 --no-open", + "build-storybook": "storybook build" }, "dependencies": { - "@headlessui/react": "2.2.2", + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@headlessui/react": "2.2.10", "@heroicons/react": "2.2.0", - "@tailwindcss/vite": "4.1.5", + "@tailwindcss/vite": "4.3.0", "@tanstack/react-query": "5.75.0", - "@tanstack/react-query-devtools": "5.75.0", + "@tanstack/react-router": "1.170.15", "clsx": "2.1.1", "graphology": "0.26.0", - "graphology-layout": "0.6.1", - "graphology-traversal": "0.3.1", "graphology-types": "0.24.8", - "graphology-utils": "2.5.2", "javascript-time-ago": "2.5.11", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-router-dom": "7.5.3", + "react": "19.2.7", + "react-dom": "19.2.7", "react-time-ago": "7.3.3", "react-zoom-pan-pinch": "3.7.0", - "wouter": "3.7.0" + "zod": "4.4.3" }, "devDependencies": { - "@eslint/js": "9.25.1", - "@testing-library/dom": "10.4.0", - "@testing-library/react": "16.3.0", + "@chromatic-com/storybook": "5.2.1", + "@eslint/js": "10.0.1", + "@hey-api/openapi-ts": "0.98.1", + "@storybook/addon-docs": "10.4.2", + "@storybook/addon-themes": "10.4.2", + "@storybook/addon-vitest": "10.4.2", + "@storybook/builder-vite": "10.4.2", + "@storybook/tanstack-react": "10.4.2", + "@tanstack/react-store": "0.9.3", + "@tanstack/router-plugin": "1.168.18", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", "@types/js-yaml": "4.0.9", - "@types/react": "19.1.2", - "@types/react-dom": "19.1.3", - "@vitejs/plugin-react": "4.4.1", - "@vitest/coverage-v8": "3.1.2", - "@vitest/eslint-plugin": "1.1.44", + "@types/node": "25.9.2", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.2", + "@vitest/browser": "4.1.8", + "@vitest/browser-playwright": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/eslint-plugin": "1.6.19", + "@vitest/ui": "4.1.8", "autoprefixer": "10.4.21", - "eslint": "9.25.1", - "eslint-config-prettier": "10.1.2", - "eslint-plugin-prettier": "5.2.6", - "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-react-refresh": "0.4.20", - "globals": "16.0.0", - "jsdom": "26.1.0", - "msw": "2.7.5", - "prettier": "3.5.3", + "eslint": "10.4.1", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.2", + "eslint-plugin-storybook": "10.4.2", + "globals": "17.6.0", + "jsdom": "29.1.1", + "msw": "2.14.6", + "msw-storybook-addon": "2.0.7", + "playwright": "1.60.0", + "prettier": "3.8.3", "prop-types": "15.8.1", - "typescript": "5.8.3", - "typescript-eslint": "8.31.1", - "vite": "6.3.4", - "vite-tsconfig-paths": "5.1.4", - "vitest": "3.1.2", - "vitest-github-actions-reporter": "0.11.1" + "storybook": "10.4.2", + "tailwindcss": "4.3.0", + "typescript": "6.0.3", + "typescript-eslint": "8.60.1", + "use-sync-external-store": "1.6.0", + "vite": "8.0.16", + "vitest": "4.1.8" }, "volta": { "node": "23.11.0", "npm": "10.9.2" }, "msw": { - "workerDirectory": "public" + "workerDirectory": [ + "public" + ] + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "@tailwindcss/oxide", + "msw" + ] } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..2e50de1 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,5901 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fontsource-variable/inter': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource-variable/jetbrains-mono': + specifier: ^5.2.8 + version: 5.2.8 + '@headlessui/react': + specifier: 2.2.10 + version: 2.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@heroicons/react': + specifier: 2.2.0 + version: 2.2.0(react@19.2.7) + '@tailwindcss/vite': + specifier: 4.3.0 + version: 4.3.0(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@tanstack/react-query': + specifier: 5.75.0 + version: 5.75.0(react@19.2.7) + '@tanstack/react-router': + specifier: 1.170.15 + version: 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + clsx: + specifier: 2.1.1 + version: 2.1.1 + graphology: + specifier: 0.26.0 + version: 0.26.0(graphology-types@0.24.8) + graphology-types: + specifier: 0.24.8 + version: 0.24.8 + javascript-time-ago: + specifier: 2.5.11 + version: 2.5.11 + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + react-time-ago: + specifier: 7.3.3 + version: 7.3.3(javascript-time-ago@2.5.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-zoom-pan-pinch: + specifier: 3.7.0 + version: 3.7.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + zod: + specifier: 4.4.3 + version: 4.4.3 + devDependencies: + '@chromatic-com/storybook': + specifier: 5.2.1 + version: 5.2.1(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + '@eslint/js': + specifier: 10.0.1 + version: 10.0.1(eslint@10.4.1(jiti@2.7.0)) + '@hey-api/openapi-ts': + specifier: 0.98.1 + version: 0.98.1(magicast@0.5.3)(typescript@6.0.3) + '@storybook/addon-docs': + specifier: 10.4.2 + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@storybook/addon-themes': + specifier: 10.4.2 + version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + '@storybook/addon-vitest': + specifier: 10.4.2 + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8)(@vitest/runner@4.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.8) + '@storybook/builder-vite': + specifier: 10.4.2 + version: 10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@storybook/tanstack-react': + specifier: 10.4.2 + version: 10.4.2(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@tanstack/router-core@1.171.13)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@tanstack/react-store': + specifier: 0.9.3 + version: 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-plugin': + specifier: 1.168.18 + version: 1.168.18(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@testing-library/dom': + specifier: 10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/js-yaml': + specifier: 4.0.9 + version: 4.0.9 + '@types/node': + specifier: 25.9.2 + version: 25.9.2 + '@types/react': + specifier: 19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: 6.0.2 + version: 6.0.2(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@vitest/browser': + specifier: 4.1.8 + version: 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + '@vitest/browser-playwright': + specifier: 4.1.8 + version: 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + '@vitest/coverage-v8': + specifier: 4.1.8 + version: 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8) + '@vitest/eslint-plugin': + specifier: 1.6.19 + version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8) + '@vitest/ui': + specifier: 4.1.8 + version: 4.1.8(vitest@4.1.8) + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.15) + eslint: + specifier: 10.4.1 + version: 10.4.1(jiti@2.7.0) + eslint-config-prettier: + specifier: 10.1.8 + version: 10.1.8(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: 5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3) + eslint-plugin-react-hooks: + specifier: 7.1.1 + version: 7.1.1(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-react-refresh: + specifier: 0.5.2 + version: 0.5.2(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-storybook: + specifier: 10.4.2 + version: 10.4.2(eslint@10.4.1(jiti@2.7.0))(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) + globals: + specifier: 17.6.0 + version: 17.6.0 + jsdom: + specifier: 29.1.1 + version: 29.1.1 + msw: + specifier: 2.14.6 + version: 2.14.6(@types/node@25.9.2)(typescript@6.0.3) + msw-storybook-addon: + specifier: 2.0.7 + version: 2.0.7(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3)) + playwright: + specifier: 1.60.0 + version: 1.60.0 + prettier: + specifier: 3.8.3 + version: 3.8.3 + prop-types: + specifier: 15.8.1 + version: 15.8.1 + storybook: + specifier: 10.4.2 + version: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tailwindcss: + specifier: 4.3.0 + version: 4.3.0 + typescript: + specifier: 6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: 8.60.1 + version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + use-sync-external-store: + specifier: 1.6.0 + version: 1.6.0(react@19.2.7) + vite: + specifier: 8.0.16 + version: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + vitest: + specifier: 4.1.8 + version: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@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.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + 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.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + 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.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@chromatic-com/storybook@5.2.1': + resolution: {integrity: sha512-z6I7NJk/0VngA64y5TNYaB4Hc2X8+90n4op6lBt9PvWk5TmIlFLDqdX33rlrwbNRkkYijVgA/wO04rVYXi5Mlg==} + engines: {node: '>=20.0.0', yarn: '>=1.22.18'} + peerDependencies: + storybook: ^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.1': + resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.5': + resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@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.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + 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 + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + + '@fontsource-variable/jetbrains-mono@5.2.8': + resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==} + + '@headlessui/react@2.2.10': + resolution: {integrity: sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} + peerDependencies: + react: '>= 16 || ^19.0.0-rc' + + '@hey-api/codegen-core@0.8.4': + resolution: {integrity: sha512-IF8/+midxK0Hu0wL/yMUfBL4+MSiRV5hziKMjKOXXqknUYE8gkf0BKW4XPsPo0DTzNDEV9qBAENaa3PRTduupQ==} + engines: {node: '>=22.18.0'} + + '@hey-api/json-schema-ref-parser@1.4.3': + resolution: {integrity: sha512-UzGSDzh3QUhrnwl4atnHc2YqDO6KemYVEOwl1Ynowm/tcr0XlpdHOpyWr5UaWIJfiXTXdYRIC9k2Yxm19pcPzQ==} + engines: {node: '>=22.18.0'} + + '@hey-api/openapi-ts@0.98.1': + resolution: {integrity: sha512-TTQQwiOTPK23kVykuMDMz2ko1dTcrJb9uA3BA7pY8vrPRGtKchLfegzl3IwqHAg4Ay0p8ByguVUdZrqe+OpLIQ==} + engines: {node: '>=22.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' + + '@hey-api/shared@0.4.7': + resolution: {integrity: sha512-ujMNKbSswtrj6mCWnoFzf8HjFJ8lt/yqQObHqMtwPXH7/KOjKe2SXtea9Si+FeYUIhJ3VpOkV5eeFCPk1LXn7g==} + engines: {node: '>=22.18.0'} + + '@hey-api/spec-types@0.2.0': + resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==} + + '@hey-api/types@0.1.4': + resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==} + + '@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'} + + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + + '@internationalized/string@3.2.9': + resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} + + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@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.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@neoconfetti/react@1.0.0': + resolution: {integrity: sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@oxc-resolver/binding-android-arm-eabi@11.20.0': + resolution: {integrity: sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.20.0': + resolution: {integrity: sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.20.0': + resolution: {integrity: sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.20.0': + resolution: {integrity: sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.20.0': + resolution: {integrity: sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0': + resolution: {integrity: sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.20.0': + resolution: {integrity: sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.20.0': + resolution: {integrity: sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.20.0': + resolution: {integrity: sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': + resolution: {integrity: sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': + resolution: {integrity: sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': + resolution: {integrity: sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': + resolution: {integrity: sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.20.0': + resolution: {integrity: sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.20.0': + resolution: {integrity: sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.20.0': + resolution: {integrity: sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.20.0': + resolution: {integrity: sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': + resolution: {integrity: sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.20.0': + resolution: {integrity: sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw==} + cpu: [x64] + os: [win32] + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@react-aria/focus@3.22.1': + resolution: {integrity: sha512-CPxtkyrBi/HYY5P3lE/57sQ6qfa0lN8E55TOm89H0kNGv0lKt+/0zP7lWERzBjRr5IxBVrQX4gFEowBN52LPaA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.28.1': + resolution: {integrity: sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.35.0': + resolution: {integrity: sha512-iNWvuzEwANttpQpdlu8nPBtdHb0mcCMj1ZTH//iRB5E/14IAnyRlR25rxH7pNLyzHINsPGEKnWvpwDMCT6vziQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@storybook/addon-docs@10.4.2': + resolution: {integrity: sha512-CtW1O4xSKZPNtpWgpfp4yB/x4pj/of+3MvlEDfErSlr3Hp3QmEa2pCLaecR08H5LJqJFlt1PtG0UrIynTvgW9w==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.2 + peerDependenciesMeta: + '@types/react': + optional: true + + '@storybook/addon-themes@10.4.2': + resolution: {integrity: sha512-e5rP/RLujp06wmO75njxnfi3ndOBbgHIXceAsuN870BVh7D7iGx+8bwslIWy+FwRBYsCMCnWk+d25Zp4ai3p4w==} + peerDependencies: + storybook: ^10.4.2 + + '@storybook/addon-vitest@10.4.2': + resolution: {integrity: sha512-gq1ZVr0IOLgiS2PzaJqHxKLczwrXvA5g3W2k/FVJA19fJNFNUut1IyQCQRSJTXLLXbnNaa8I0wjGGc2BoLarJg==} + peerDependencies: + '@vitest/browser': ^3.0.0 || ^4.0.0 + '@vitest/browser-playwright': ^4.0.0 + '@vitest/runner': ^3.0.0 || ^4.0.0 + storybook: ^10.4.2 + vitest: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/runner': + optional: true + vitest: + optional: true + + '@storybook/builder-vite@10.4.2': + resolution: {integrity: sha512-d3+i9vbbUfV6hvT90qabmy1WmC4bEJ7iAYDm0217doeA+S6awF25GF0qOy9gN9waU4NMntHoVpdB1YQO2wUj/w==} + peerDependencies: + storybook: ^10.4.2 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/csf-plugin@10.4.2': + resolution: {integrity: sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.4.2 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.0.2': + resolution: {integrity: sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/react-dom-shim@10.4.2': + resolution: {integrity: sha512-Eng3Yt2NCjPX94QcfyLeUFhrMj0hec2yU9J/qafBVbfj9XrFI8o+0ZwYJ7uXb9ECbvPN4y06dgt/2W/LiR417w==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.2 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@storybook/react-vite@10.4.2': + resolution: {integrity: sha512-nGrS74p0ujPSQ7qD5jKLLlao2igfTTb6Kf/uRhVV8XM0uM7WvxR9K20ddCM8jFmx1jY9fRm0UBGaeaXHDIh5SA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.2 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/react@10.4.2': + resolution: {integrity: sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.2 + typescript: '>= 4.9.x' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + typescript: + optional: true + + '@storybook/tanstack-react@10.4.2': + resolution: {integrity: sha512-d9cTzb7o0+SHE+nvwlalMIwgUWRD5x+DbfyjthymUftDeX7Bbyy0W8nzM2v/+EhydWOVfAjNfZ35HYMUlyM/Iw==} + peerDependencies: + '@tanstack/react-router': ^1.168.10 + '@tanstack/react-start': ^1.167.16 + '@tanstack/router-core': ^1.168.9 + '@tanstack/start-client-core': ^1.167.9 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.2 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + '@tanstack/start-client-core': + optional: true + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/history@1.162.0': + resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} + engines: {node: '>=20.19'} + + '@tanstack/query-core@5.75.0': + resolution: {integrity: sha512-rk8KQuCdhoRkzjRVF3QxLgAfFUyS0k7+GCQjlGEpEGco+qazJ0eMH6aO1DjDjibH7/ik383nnztua3BG+lOnwg==} + + '@tanstack/react-query@5.75.0': + resolution: {integrity: sha512-H+TNgxmTbzH8qQ5MT5xsZEhQ8BG1tUYduDSfeAOzroVZgd/AEjg1rRYSP/9Tl9/hPobZ7iZzV401n77kStrbKw==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router@1.170.15': + resolution: {integrity: sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-virtual@3.14.2': + resolution: {integrity: sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.171.13': + resolution: {integrity: sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.167.17': + resolution: {integrity: sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.18': + resolution: {integrity: sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.15 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.2': + resolution: {integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==} + engines: {node: '>=20.19'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + + '@tanstack/virtual-core@3.17.0': + resolution: {integrity: sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==} + + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + 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 + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdx@2.0.14': + resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} + + '@types/node@25.9.2': + resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} + 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.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} + 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.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + 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.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/browser-playwright@4.1.8': + resolution: {integrity: sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==} + peerDependencies: + playwright: '*' + vitest: 4.1.8 + + '@vitest/browser@4.1.8': + resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==} + peerDependencies: + vitest: 4.1.8 + + '@vitest/coverage-v8@4.1.8': + resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==} + peerDependencies: + '@vitest/browser': 4.1.8 + vitest: 4.1.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/eslint-plugin@1.6.19': + resolution: {integrity: sha512-zodmXRsVKFsuHxHJILuTFaaKsrsxm0YsiOX65clk+LpCW9JrVXaf6ERXr0caDs+NEk0S62Jyk0K7XYQ7gWXheA==} + engines: {node: '>=18'} + peerDependencies: + '@typescript-eslint/eslint-plugin': '*' + eslint: '>=8.57.0' + typescript: '>=5.0.0' + vitest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + typescript: + optional: true + vitest: + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + + '@vitest/ui@4.1.8': + resolution: {integrity: sha512-RUS2ZU2TsduVrI+9c12uTNaKrNUTsm6yFt3fueEUB9iKvyC2UP83F+sqIz00HQIah4UOL1TMoDAki8K0NjGvsA==} + peerDependencies: + vitest: 4.1.8 + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + 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'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + ast-v8-to-istanbul@1.0.3: + resolution: {integrity: sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.34: + resolution: {integrity: sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + caniuse-lite@1.0.30001797: + resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==} + + 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'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chromatic@16.10.0: + resolution: {integrity: sha512-nFsztmnu7rFiGafUJgXvLUNpqmRylz92eNvzBoJNTKKQj4EQUyxznwnfpf1dTs7hXtWD8JwcH92jADydaHA1sw==} + hasBin: true + peerDependencies: + '@chromatic-com/cypress': ^0.*.* || ^1.0.0 + '@chromatic-com/playwright': ^0.*.* || ^1.0.0 + '@chromatic-com/vitest': ^0.*.* || ^1.0.0 + peerDependenciesMeta: + '@chromatic-com/cypress': + optional: true + '@chromatic-com/playwright': + optional: true + '@chromatic-com/vitest': + optional: true + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + 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==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + 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==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + 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==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + electron-to-chromium@1.5.368: + resolution: {integrity: sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + enhanced-resolve@5.23.0: + resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==} + engines: {node: '>=10.13.0'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + 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-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + 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-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-plugin-storybook@10.4.2: + resolution: {integrity: sha512-l3/vzLRmb8VSi3X1Bo6/Pa+64naw1jFsZE5jPPA4izvVdNhH1rF4rGuOC3kDTU926qKVBQtKua8D24XWQtvcGg==} + peerDependencies: + eslint: '>=8' + storybook: ^10.4.2 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + 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@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + 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@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + 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'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + 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.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + 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==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + giget@3.2.0: + resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphology-types@0.24.8: + resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==} + + graphology@0.26.0: + resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==} + peerDependencies: + graphology-types: '>=0.24.0' + + graphql@16.14.1: + resolution: {integrity: sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + + 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@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + 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'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isbot@5.1.41: + resolution: {integrity: sha512-9WFV/Vhh0FEj6CQ7MoHweEL9/vLKPjeoD2I2htbAjX7kbW7VJs3OCpWOVyd+JraNTWVU6/DRx2MZy2KaUNXHcg==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + javascript-time-ago@2.5.11: + resolution: {integrity: sha512-Zeyf5R7oM1fSMW9zsU3YgAYwE0bimEeF54Udn2ixGd8PUwu+z1Yc5t4Y8YScJDMHD6uCx6giLt3VJR5K4CMwbg==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.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@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + 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@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + 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==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw-storybook-addon@2.0.7: + resolution: {integrity: sha512-TGmlxXy2TsaB6QcClVKRxqvay5f93xoLguHOihRFQ+gIEIyiyvcoQjkEeuOe7Y9qvddzGB1LyFomzPo9/EpnuQ==} + peerDependencies: + msw: ^2.0.0 + + msw@2.14.6: + resolution: {integrity: sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.2: + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.20.0: + resolution: {integrity: sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==} + + 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'} + + 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==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + 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'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + 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'} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + react-aria@3.49.0: + resolution: {integrity: sha512-4+oK9FwJQWYhyA5zLfj/feOGY0zZbkE1muoF4gyxMroHVypjcYaRSTlJwvxph2zIlxt757KX6xIK2wJ5Aw1Kog==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + 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-stately@3.47.0: + resolution: {integrity: sha512-H3ar+SOWP920EbVg7qWfP3fZjZiwhlEJAEJQqjt+w8oKijCwFgr0+R4941PIHscOXRNRvEOjvWilitImC0DdBg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-time-ago@7.3.3: + resolution: {integrity: sha512-5kh2Kuu/UhHzcZrGvf3GUrF2d+IXjkIXif5MR2iDWIfSqQuBW27/ejN/tmzJBRyPiryYTgbDIG6AZFJ4RW3yfw==} + peerDependencies: + javascript-time-ago: ^2.3.7 + react: '>=0.16.8' + react-dom: '>=0.16.8' + + react-zoom-pan-pinch@3.7.0: + resolution: {integrity: sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + react: '*' + react-dom: '*' + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + relative-time-format@1.1.12: + resolution: {integrity: sha512-qaZBjmRIuXLfuLnzgqpFdBPa5W0euSX1tMnoMUHGPphLwJmrt8xbNiOIHrlvYOD6oNJ0M5owPCZyPibI8de5pQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.2: + resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + engines: {node: '>=10'} + hasBin: true + + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} + + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + + 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'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + storybook@10.4.2: + resolution: {integrity: sha512-5Ax5vbHxFgMBGGhQDm75Rrumm/HZC4ICFhMcJaM0UlqnC/4FKj/IaZtImZFupknyiiyUEcWHPQFA2kX3/VSv1A==} + hasBin: true + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 + peerDependenciesMeta: + '@types/react': + optional: true + prettier: + optional: true + vite-plus: + optional: true + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + 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-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + 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==} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.2: + resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} + + tldts@7.4.2: + resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} + hasBin: true + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + 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' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + 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'} + + type-fest@5.7.0: + resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} + engines: {node: '>=20'} + + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} + 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@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@7.27.2: + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} + engines: {node: '>=20.18.1'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + 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@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + 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@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + 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'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + 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 + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + 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==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + 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.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@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.7 + '@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.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@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.28.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.7 + '@babel/types': 7.29.7 + 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.7 + 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.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@blazediff/core@1.9.1': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@chromatic-com/storybook@5.2.1(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 16.10.0 + jsonfile: 6.2.1 + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + strip-ansi: 7.2.0 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + - '@chromatic-com/vitest' + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.9.2': + 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.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': + dependencies: + eslint: 10.4.1(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.4.1(jiti@2.7.0))': + optionalDependencies: + eslint: 10.4.1(jiti@2.7.0) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.15.1': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/react@0.26.28(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + tabbable: 6.4.0 + + '@floating-ui/utils@0.2.11': {} + + '@fontsource-variable/inter@5.2.8': {} + + '@fontsource-variable/jetbrains-mono@5.2.8': {} + + '@headlessui/react@2.2.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@react-aria/focus': 3.22.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@react-aria/interactions': 3.28.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-virtual': 3.14.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + + '@heroicons/react@2.2.0(react@19.2.7)': + dependencies: + react: 19.2.7 + + '@hey-api/codegen-core@0.8.4(magicast@0.5.3)': + dependencies: + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + c12: 3.3.4(magicast@0.5.3) + color-support: 1.1.3 + transitivePeerDependencies: + - magicast + + '@hey-api/json-schema-ref-parser@1.4.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + + '@hey-api/openapi-ts@0.98.1(magicast@0.5.3)(typescript@6.0.3)': + dependencies: + '@hey-api/codegen-core': 0.8.4(magicast@0.5.3) + '@hey-api/json-schema-ref-parser': 1.4.3 + '@hey-api/shared': 0.4.7(magicast@0.5.3) + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + '@lukeed/ms': 2.0.2 + ansi-colors: 4.1.3 + color-support: 1.1.3 + commander: 14.0.3 + get-tsconfig: 4.14.0 + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + + '@hey-api/shared@0.4.7(magicast@0.5.3)': + dependencies: + '@hey-api/codegen-core': 0.8.4(magicast@0.5.3) + '@hey-api/json-schema-ref-parser': 1.4.3 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + cross-spawn: 7.0.6 + open: 11.0.0 + semver: 7.7.4 + transitivePeerDependencies: + - magicast + + '@hey-api/spec-types@0.2.0': + dependencies: + '@hey-api/types': 0.1.4 + + '@hey-api/types@0.1.4': {} + + '@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': {} + + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.1.1(@types/node@25.9.2)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@25.9.2) + '@inquirer/type': 4.0.7(@types/node@25.9.2) + optionalDependencies: + '@types/node': 25.9.2 + + '@inquirer/core@11.2.1(@types/node@25.9.2)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@25.9.2) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 25.9.2 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/type@4.0.7(@types/node@25.9.2)': + optionalDependencies: + '@types/node': 25.9.2 + + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/string@3.2.9': + dependencies: + '@swc/helpers': 0.5.23 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + optionalDependencies: + typescript: 6.0.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@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.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jsdevtools/ono@7.1.3': {} + + '@lukeed/ms@2.0.2': {} + + '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@types/mdx': 2.0.14 + '@types/react': 19.2.17 + react: 19.2.7 + + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@1.1.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.2 + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@neoconfetti/react@1.0.0': {} + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-project/types@0.133.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.20.0': + optional: true + + '@oxc-resolver/binding-android-arm64@11.20.0': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.20.0': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.20.0': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.20.0': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.20.0': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.20.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.20.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.20.0': + optional: true + + '@pkgr/core@0.3.6': {} + + '@polka/url@1.0.0-next.29': {} + + '@react-aria/focus@3.22.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@swc/helpers': 0.5.23 + react: 19.2.7 + react-aria: 3.49.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-dom: 19.2.7(react@19.2.7) + + '@react-aria/interactions@3.28.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@react-types/shared': 3.35.0(react@19.2.7) + '@swc/helpers': 0.5.23 + react: 19.2.7 + react-aria: 3.49.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-dom: 19.2.7(react@19.2.7) + + '@react-types/shared@3.35.0(react@19.2.7)': + dependencies: + react: 19.2.7 + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/pluginutils@5.4.0': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + + '@standard-schema/spec@1.1.0': {} + + '@storybook/addon-docs@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) + '@storybook/csf-plugin': 10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@storybook/react-dom-shim': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + ts-dedent: 2.2.0 + optionalDependencies: + '@types/react': 19.2.17 + transitivePeerDependencies: + - '@types/react-dom' + - esbuild + - rollup + - vite + - webpack + + '@storybook/addon-themes@10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + dependencies: + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + ts-dedent: 2.2.0 + + '@storybook/addon-vitest@10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8)(@vitest/runner@4.1.8)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vitest@4.1.8)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + optionalDependencies: + '@vitest/browser': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + '@vitest/browser-playwright': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + '@vitest/runner': 4.1.8 + vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/builder-vite@10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@storybook/csf-plugin': 10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + ts-dedent: 2.2.0 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.27.7 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + + '@storybook/global@5.0.0': {} + + '@storybook/icons@2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@storybook/react-dom-shim@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@storybook/react-vite@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@rollup/pluginutils': 5.4.0 + '@storybook/builder-vite': 10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@storybook/react': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 19.2.7 + react-docgen: 8.0.3 + react-dom: 19.2.7(react@19.2.7) + resolve: 1.22.12 + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tsconfig-paths: 4.2.0 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - typescript + - webpack + + '@storybook/react@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + react: 19.2.7 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + react-dom: 19.2.7(react@19.2.7) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@storybook/tanstack-react@10.4.2(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@tanstack/router-core@1.171.13)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@storybook/builder-vite': 10.4.2(esbuild@0.27.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@storybook/react': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3) + '@storybook/react-vite': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.27.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@tanstack/react-router': 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.13 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - typescript + - webpack + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.23.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + + '@tanstack/history@1.162.0': {} + + '@tanstack/query-core@5.75.0': {} + + '@tanstack/react-query@5.75.0(react@19.2.7)': + dependencies: + '@tanstack/query-core': 5.75.0 + react: 19.2.7 + + '@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/history': 1.162.0 + '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.13 + isbot: 5.1.41 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + + '@tanstack/react-virtual@3.14.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/virtual-core': 3.17.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@tanstack/router-core@1.171.13': + dependencies: + '@tanstack/history': 1.162.0 + cookie-es: 3.1.1 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + '@tanstack/router-generator@1.167.17': + dependencies: + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.13 + '@tanstack/router-utils': 1.162.2 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.8.3 + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.18(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.13 + '@tanstack/router-generator': 1.167.17 + '@tanstack/router-utils': 1.162.2 + chokidar: 5.0.0 + unplugin: 3.0.0 + zod: 4.4.3 + optionalDependencies: + '@tanstack/react-router': 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.162.2': + dependencies: + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + ansis: 4.3.1 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.9.3': {} + + '@tanstack/virtual-core@3.17.0': {} + + '@tanstack/virtual-file-routes@1.162.0': {} + + '@testing-library/dom@10.4.1': + 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.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/doctrine@0.0.9': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/mdx@2.0.14': {} + + '@types/node@25.9.2': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/resolve@1.20.6': {} + + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 25.9.2 + + '@types/statuses@2.0.6': {} + + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.60.1': {} + + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.2 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + + '@vitest/browser-playwright@4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8)': + dependencies: + '@vitest/browser': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + '@vitest/mocker': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + playwright: 1.60.0 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/coverage-v8@4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.8 + ast-v8-to-istanbul: 1.0.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.2 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + optionalDependencies: + '@vitest/browser': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + + '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + typescript: 6.0.3 + vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/expect@4.1.8': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.14.6(@types/node@25.9.2)(typescript@6.0.3) + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': + dependencies: + '@vitest/utils': 4.1.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/spy@4.1.8': {} + + '@vitest/ui@4.1.8(vitest@4.1.8)': + dependencies: + '@vitest/utils': 4.1.8 + fflate: 0.8.3 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@webcontainer/env@1.1.1': {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + 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-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansis@4.3.1: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + ast-v8-to-istanbul@1.0.3: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + autoprefixer@10.4.21(postcss@8.5.15): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001797 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.34: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.34 + caniuse-lite: 1.0.30001797 + electron-to-chromium: 1.5.368 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.4(magicast@0.5.3): + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.0.8 + giget: 3.2.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + optionalDependencies: + magicast: 0.5.3 + + caniuse-lite@1.0.30001797: {} + + 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: {} + + check-error@2.1.3: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chromatic@16.10.0: + dependencies: + semver: 7.8.2 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + commander@14.0.3: {} + + confbox@0.2.4: {} + + convert-source-map@2.0.0: {} + + cookie-es@3.1.1: {} + + cookie@1.1.1: {} + + 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 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dotenv@17.4.2: {} + + electron-to-chromium@1.5.368: {} + + emoji-regex@8.0.0: {} + + empathic@2.0.1: {} + + enhanced-resolve@5.23.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.0.0: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)): + dependencies: + eslint: 10.4.1(jiti@2.7.0) + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3): + dependencies: + eslint: 10.4.1(jiti@2.7.0) + prettier: 3.8.3 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.7.0)) + + eslint-plugin-react-hooks@7.1.1(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 10.4.1(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@10.4.1(jiti@2.7.0)): + dependencies: + eslint: 10.4.1(jiti@2.7.0) + + eslint-plugin-storybook@10.4.2(eslint@10.4.1(jiti@2.7.0))(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3): + dependencies: + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) + storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.1(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@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 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.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 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esprima@4.0.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@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + events@3.3.0: {} + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fflate@0.8.3: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.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.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fraction.js@4.3.7: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@3.2.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@17.6.0: {} + + graceful-fs@4.2.11: {} + + graphology-types@0.24.8: {} + + graphology@0.26.0(graphology-types@0.24.8): + dependencies: + events: 3.3.0 + graphology-types: 0.24.8 + + graphql@16.14.1: {} + + has-flag@4.0.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.0 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-node-process@1.2.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isbot@5.1.41: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + javascript-time-ago@2.5.11: + dependencies: + relative-time-format: 1.1.12 + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.27.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@11.5.1: {} + + 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.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.2 + + mdn-data@2.27.1: {} + + memoize-one@6.0.0: {} + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + msw-storybook-addon@2.0.7(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3)): + dependencies: + is-node-process: 1.2.0 + msw: 2.14.6(@types/node@25.9.2)(typescript@6.0.3) + + msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@25.9.2) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.1 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.7.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@3.0.0: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.47: {} + + normalize-range@0.1.2: {} + + object-assign@4.1.1: {} + + obug@2.1.2: {} + + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + 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 + + outvariant@1.4.3: {} + + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.20.0: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.20.0 + '@oxc-resolver/binding-android-arm64': 11.20.0 + '@oxc-resolver/binding-darwin-arm64': 11.20.0 + '@oxc-resolver/binding-darwin-x64': 11.20.0 + '@oxc-resolver/binding-freebsd-x64': 11.20.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.20.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.20.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.20.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.20.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.20.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.20.0 + '@oxc-resolver/binding-linux-x64-musl': 11.20.0 + '@oxc-resolver/binding-openharmony-arm64': 11.20.0 + '@oxc-resolver/binding-wasm32-wasi': 11.20.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.20.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.20.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@2.1.0: {} + + performance-now@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.8.3: {} + + 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: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-aria@3.49.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.35.0(react@19.2.7) + '@swc/helpers': 0.5.23 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-stately: 3.47.0(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + + react-docgen-typescript@2.4.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + react-docgen@8.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-stately@3.47.0(react@19.2.7): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.35.0(react@19.2.7) + '@swc/helpers': 0.5.23 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + + react-time-ago@7.3.3(javascript-time-ago@2.5.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + javascript-time-ago: 2.5.11 + memoize-one: 6.0.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + react-zoom-pan-pinch@3.7.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + react@19.2.7: {} + + readdirp@5.0.0: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + relative-time-format@1.1.12: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rettime@0.11.11: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + run-applescript@7.1.0: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + semver@7.8.2: {} + + seroval-plugins@1.5.4(seroval@1.5.4): + dependencies: + seroval: 1.5.4 + + seroval@1.5.4: {} + + set-cookie-parser@3.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.27.7 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.20.0 + recast: 0.23.11 + semver: 7.8.2 + use-sync-external-store: 1.6.0(react@19.2.7) + ws: 8.21.0 + optionalDependencies: + '@types/react': 19.2.17 + prettier: 3.8.3 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - react + - react-dom + - utf-8-validate + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + tabbable@6.4.0: {} + + tagged-tag@1.0.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@2.0.0: {} + + tinyrainbow@3.1.0: {} + + tinyspy@4.0.4: {} + + tldts-core@7.4.2: {} + + tldts@7.4.2: + dependencies: + tldts-core: 7.4.2 + + totalist@3.0.1: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.4.2 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + ts-dedent@2.2.0: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.7.0: + dependencies: + tagged-tag: 1.0.0 + + typescript-eslint@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + undici@7.27.2: {} + + universalify@2.0.1: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + + vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.2 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@4.1.8(@types/node@25.9.2)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.2 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.2 + '@vitest/browser-playwright': 4.1.8(msw@2.14.6(@types/node@25.9.2)(typescript@6.0.3))(playwright@1.60.0)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.27.7)(jiti@2.7.0))(vitest@4.1.8) + '@vitest/coverage-v8': 4.1.8(@vitest/browser@4.1.8)(vitest@4.1.8) + '@vitest/ui': 4.1.8(vitest@4.1.8) + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + 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: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.21.0: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/web/public/apple-touch-icon.png b/web/public/apple-touch-icon.png index c2f81d4..ebced47 100644 Binary files a/web/public/apple-touch-icon.png and b/web/public/apple-touch-icon.png differ diff --git a/web/public/favicon-96x96.png b/web/public/favicon-96x96.png index 5c80003..a604849 100644 Binary files a/web/public/favicon-96x96.png and b/web/public/favicon-96x96.png differ diff --git a/web/public/favicon.ico b/web/public/favicon.ico index c096145..7ba49dc 100644 Binary files a/web/public/favicon.ico and b/web/public/favicon.ico differ diff --git a/web/public/favicon.svg b/web/public/favicon.svg index 0e4f6ea..3cafc98 100644 --- a/web/public/favicon.svg +++ b/web/public/favicon.svg @@ -1,3 +1,17 @@ - \ No newline at end of file + + + + + + + + + + + diff --git a/web/public/humans.txt b/web/public/humans.txt deleted file mode 100644 index 98ce756..0000000 --- a/web/public/humans.txt +++ /dev/null @@ -1,3 +0,0 @@ -Team: EF ethPandaOps -Twitter: @savid @samcmau -Github: @savid @samcm diff --git a/web/public/mockServiceWorker.js b/web/public/mockServiceWorker.js index 8b841ba..33dde9e 100644 --- a/web/public/mockServiceWorker.js +++ b/web/public/mockServiceWorker.js @@ -5,24 +5,23 @@ * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. - * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.7.5' -const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const PACKAGE_VERSION = '2.14.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() -self.addEventListener('install', function () { +addEventListener('install', function () { self.skipWaiting() }) -self.addEventListener('activate', function (event) { +addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()) }) -self.addEventListener('message', async function (event) { - const clientId = event.source.id +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') if (!clientId || !self.clients) { return @@ -72,11 +71,6 @@ self.addEventListener('message', async function (event) { break } - case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - break - } - case 'CLIENT_CLOSED': { activeClientIds.delete(clientId) @@ -94,69 +88,92 @@ self.addEventListener('message', async function (event) { } }) -self.addEventListener('fetch', function (event) { - const { request } = event +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() // Bypass navigation requests. - if (request.mode === 'navigate') { + if (event.request.mode === 'navigate') { return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). + // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { return } - // Generate unique request ID. const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId)) + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) }) -async function handleRequest(event, requestId) { +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - ;(async function () { - const responseClone = response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { type: responseClone.type, status: responseClone.status, statusText: responseClone.statusText, - body: responseClone.body, headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, }, }, - [responseClone.body], - ) - })() + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) } return response } -// Resolve the main client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) @@ -184,12 +201,17 @@ async function resolveMainClient(event) { }) } -async function getResponse(event, client, requestId) { - const { request } = event - +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = request.clone() + const requestClone = event.request.clone() function passthrough() { // Cast the request headers to a new Headers instance @@ -230,29 +252,18 @@ async function getResponse(event, client, requestId) { } // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer() + const serializedRequest = await serializeRequest(event.request) const clientMessage = await sendToClient( client, { type: 'REQUEST', payload: { id: requestId, - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: requestBuffer, - keepalive: request.keepalive, + interceptedAt: requestInterceptedAt, + ...serializedRequest, }, }, - [requestBuffer], + [serializedRequest.body], ) switch (clientMessage.type) { @@ -268,6 +279,12 @@ async function getResponse(event, client, requestId) { return passthrough() } +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() @@ -280,14 +297,18 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data) } - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ) + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) }) } -async function respondWithMock(response) { +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { // Setting response status code to 0 is a no-op. // However, when responding with a "Response.error()", the produced Response // instance will have status code set to 0. Since it's not possible to create @@ -305,3 +326,24 @@ async function respondWithMock(response) { return mockedResponse } + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/web/public/site.webmanifest b/web/public/site.webmanifest index ccf313a..eda3d17 100644 --- a/web/public/site.webmanifest +++ b/web/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "MyWebSite", - "short_name": "MySite", + "name": "Forky: Ethereum Beacon Chain Fork Choice Data Visualization & Analysis Tool", + "short_name": "Forky", "icons": [ { "src": "/web-app-manifest-192x192.png", @@ -18,4 +18,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/web/public/web-app-manifest-192x192.png b/web/public/web-app-manifest-192x192.png index b51bd12..e8d1dd0 100644 Binary files a/web/public/web-app-manifest-192x192.png and b/web/public/web-app-manifest-192x192.png differ diff --git a/web/public/web-app-manifest-512x512.png b/web/public/web-app-manifest-512x512.png index 9d5629d..fcdd4d3 100644 Binary files a/web/public/web-app-manifest-512x512.png and b/web/public/web-app-manifest-512x512.png differ diff --git a/web/src/App.stories.tsx b/web/src/App.stories.tsx new file mode 100644 index 0000000..c45c8c6 --- /dev/null +++ b/web/src/App.stories.tsx @@ -0,0 +1,127 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import App from '@app/App'; +import { storyApiHandlers } from '@app/stories/fixtures'; + +const meta = { + title: 'App/Routes', + component: App, + parameters: { + layout: 'fullscreen', + msw: { + handlers: storyApiHandlers(), + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const AggregatedLive: Story = { + parameters: { + tanstack: { + router: { + path: '/', + }, + }, + }, +}; + +export const SourceView: Story = { + args: { + node: 'syd1-lighthouse-001', + }, + parameters: { + tanstack: { + router: { + path: '/node/$', + params: { _splat: 'syd1-lighthouse-001' }, + }, + }, + }, +}; + +export const SnapshotView: Story = { + args: { + frameId: 'frame-alpha', + }, + parameters: { + tanstack: { + router: { + path: '/snapshot/$frameId', + params: { frameId: 'frame-alpha' }, + }, + }, + }, +}; + +export const BYOUploadView: Story = { + args: { + byo: true, + }, + parameters: { + tanstack: { + router: { + path: '/byo', + }, + }, + }, +}; + +export const EventsOverlay: Story = { + args: { + eventsOpen: true, + eventsCloseTo: '/', + }, + parameters: { + tanstack: { + router: { + path: '/events', + }, + }, + }, +}; + +export const NodeEventsOverlay: Story = { + args: { + node: 'ams3-teku-001', + eventsOpen: true, + eventsCloseTo: '/node/ams3-teku-001', + }, + parameters: { + tanstack: { + router: { + path: '/node/$', + params: { _splat: 'ams3-teku-001/events' }, + }, + }, + }, +}; + +export const LoadingSpec: Story = { + tags: ['test-exclude'], + parameters: { + tanstack: { + router: { + path: '/', + }, + }, + msw: { + handlers: storyApiHandlers({ specDelay: 10_000 }), + }, + }, +}; + +export const SpecError: Story = { + parameters: { + tanstack: { + router: { + path: '/', + }, + }, + msw: { + handlers: storyApiHandlers({ specError: true }), + }, + }, +}; diff --git a/web/src/App.tsx b/web/src/App.tsx index cafde38..3adf875 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import ClearTimeParam from '@components/ClearTimeParam'; import Loading from '@components/Loading'; import { ValueProps } from '@contexts/ethereum'; import { useSpecQuery, useNowQuery } from '@hooks/useQuery'; @@ -16,13 +17,22 @@ export default function App({ node, frameId, byo = false, + eventsOpen = false, + eventsCloseTo = '/', }: { node?: string; frameId?: string; byo?: boolean; + eventsOpen?: boolean; + eventsCloseTo?: string; }) { const { data, isLoading, error } = useSpecQuery(); - const { data: dataNow, isLoading: isLoadingNow, error: errorNow } = useNowQuery(); + const { + data: dataNow, + isLoading: isLoadingNow, + error: errorNow, + errorUpdatedAt: errorNowAt, + } = useNowQuery(); const formattedData = useMemo(() => { if (!data?.spec) return undefined; @@ -57,15 +67,16 @@ export default function App({ ]; } - // worst case when failing to get server now, just use the current local time - return [errorNow ? Date.now() : undefined, true]; - }, [formattedData, dataNow, errorNow]); + // worst case when failing to get server now, just use the local time at + // which the fetch failed (kept pure: no clock reads during render) + return [errorNow ? errorNowAt : undefined, true]; + }, [formattedData, dataNow, errorNow, errorNowAt]); if (isLoading || error || !formattedData || !initialTime || isLoadingNow) return ( -
+
-
+ +
- +
diff --git a/web/src/ErrorBoundary.tsx b/web/src/ErrorBoundary.tsx index eb8447f..8788520 100644 --- a/web/src/ErrorBoundary.tsx +++ b/web/src/ErrorBoundary.tsx @@ -10,6 +10,17 @@ interface State { hasError: boolean; } +export function ErrorBoundaryFallback() { + return ( +
+ +

+ Uhh... Something went wrong +

+
+ ); +} + class ErrorBoundary extends Component { public state: State = { hasError: false, @@ -26,12 +37,7 @@ class ErrorBoundary extends Component { public render() { if (this.state.hasError) { - return ( -
- -

Uhh... Something went wrong

-
- ); + return ; } return this.props.children; diff --git a/web/src/Fallbacks.stories.tsx b/web/src/Fallbacks.stories.tsx new file mode 100644 index 0000000..612affb --- /dev/null +++ b/web/src/Fallbacks.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import { ErrorBoundaryFallback as ErrorBoundaryFallbackView } from '@app/ErrorBoundary'; +import NotFound from '@app/NotFound'; + +const meta = { + title: 'App/Fallbacks', + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const NotFoundPage: Story = { + render: () => , +}; + +export const ErrorBoundaryFallback: Story = { + render: () => , +}; diff --git a/web/src/NotFound.tsx b/web/src/NotFound.tsx index d6ad468..7fcec75 100644 --- a/web/src/NotFound.tsx +++ b/web/src/NotFound.tsx @@ -2,9 +2,12 @@ import Logo from '@assets/forky_logo.png'; export default function NotFound() { return ( -
- -

Page not found

+
+ +

404

+

+ Page not found +

); } diff --git a/web/src/api/@tanstack/react-query.gen.ts b/web/src/api/@tanstack/react-query.gen.ts new file mode 100644 index 0000000..273c2ec --- /dev/null +++ b/web/src/api/@tanstack/react-query.gen.ts @@ -0,0 +1,261 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { queryOptions, type UseMutationOptions } from '@tanstack/react-query'; + +import { client } from '../client.gen'; +import { + getEthereumNow, + getEthereumSpec, + getFrame, + listMetadata, + listMetadataEpochs, + listMetadataLabels, + listMetadataNodes, + listMetadataSlots, + type Options, +} from '../sdk.gen'; +import type { + GetEthereumNowData, + GetEthereumNowError, + GetEthereumNowResponse, + GetEthereumSpecData, + GetEthereumSpecError, + GetEthereumSpecResponse, + GetFrameData, + GetFrameError, + GetFrameResponse, + ListMetadataData, + ListMetadataEpochsData, + ListMetadataEpochsError, + ListMetadataEpochsResponse, + ListMetadataError, + ListMetadataLabelsData, + ListMetadataLabelsError, + ListMetadataLabelsResponse, + ListMetadataNodesData, + ListMetadataNodesError, + ListMetadataNodesResponse, + ListMetadataResponse, + ListMetadataSlotsData, + ListMetadataSlotsError, + ListMetadataSlotsResponse, +} from '../types.gen'; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + +export const getEthereumNowQueryKey = (options?: Options) => + createQueryKey('getEthereumNow', options); + +/** + * Get the current wall-clock slot and epoch + */ +export const getEthereumNowOptions = (options?: Options) => + queryOptions< + GetEthereumNowResponse, + GetEthereumNowError, + GetEthereumNowResponse, + ReturnType + >({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEthereumNow({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEthereumNowQueryKey(options), + }); + +export const getEthereumSpecQueryKey = (options?: Options) => + createQueryKey('getEthereumSpec', options); + +/** + * Get the Ethereum network name and spec + */ +export const getEthereumSpecOptions = (options?: Options) => + queryOptions< + GetEthereumSpecResponse, + GetEthereumSpecError, + GetEthereumSpecResponse, + ReturnType + >({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEthereumSpec({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEthereumSpecQueryKey(options), + }); + +export const getFrameQueryKey = (options: Options) => createQueryKey('getFrame', options); + +/** + * Get a single fork-choice frame by ID + */ +export const getFrameOptions = (options: Options) => + queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getFrame({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getFrameQueryKey(options), + }); + +/** + * List fork-choice frame metadata + */ +export const listMetadataMutation = ( + options?: Partial> +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async fnOptions => { + const { data } = await listMetadata({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * List distinct nodes across stored frames + */ +export const listMetadataNodesMutation = ( + options?: Partial> +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions< + ListMetadataNodesResponse, + ListMetadataNodesError, + Options + > = { + mutationFn: async fnOptions => { + const { data } = await listMetadataNodes({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * List distinct slots across stored frames + */ +export const listMetadataSlotsMutation = ( + options?: Partial> +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions< + ListMetadataSlotsResponse, + ListMetadataSlotsError, + Options + > = { + mutationFn: async fnOptions => { + const { data } = await listMetadataSlots({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * List distinct epochs across stored frames + */ +export const listMetadataEpochsMutation = ( + options?: Partial> +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions< + ListMetadataEpochsResponse, + ListMetadataEpochsError, + Options + > = { + mutationFn: async fnOptions => { + const { data } = await listMetadataEpochs({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * List distinct labels across stored frames + */ +export const listMetadataLabelsMutation = ( + options?: Partial> +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions< + ListMetadataLabelsResponse, + ListMetadataLabelsError, + Options + > = { + mutationFn: async fnOptions => { + const { data } = await listMetadataLabels({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; diff --git a/web/src/api/client.gen.ts b/web/src/api/client.gen.ts new file mode 100644 index 0000000..7e3dfef --- /dev/null +++ b/web/src/api/client.gen.ts @@ -0,0 +1,22 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createClientConfig } from '../utils/api-config.ts'; + +import { type Client, type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config +) => Config & T>; + +export const client: Client = createClient( + createClientConfig(createConfig({ baseUrl: 'https://forky.mainnet.ethpandaops.io' })) +); diff --git a/web/src/api/client/client.gen.ts b/web/src/api/client/client.gen.ts new file mode 100644 index 0000000..86e62d0 --- /dev/null +++ b/web/src/api/client/client.gen.ts @@ -0,0 +1,273 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams(opts); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async options => { + const throwOnError = options.throwOnError ?? _config.throwOnError; + const responseStyle = options.responseStyle ?? _config.responseStyle; + + let request: Request | undefined; + let response: Response | undefined; + + try { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + + response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' ? getParseAs(response.headers.get('Content-Type')) : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + throw jsonError ?? textError; + } catch (error) { + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, options as ResolvedRequestOptions); + } + } + + finalError = finalError || {}; + + if (throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response, + }; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = options => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/web/src/api/client/index.ts b/web/src/api/client/index.ts new file mode 100644 index 0000000..3c62a27 --- /dev/null +++ b/web/src/api/client/index.ts @@ -0,0 +1,22 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export type { ServerSentEventsResult } from '../core/serverSentEvents.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts new file mode 100644 index 0000000..7973dd7 --- /dev/null +++ b/web/src/api/client/types.gen.ts @@ -0,0 +1,207 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { ServerSentEventsOptions, ServerSentEventsResult } from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + 'onRequest' | 'onSseError' | 'onSseEvent' | 'sseDefaultRetryDelay' | 'sseMaxRetryAttempts' | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + /** request may be undefined, because error may be from building the request object itself */ + request?: Request; + /** response may be undefined, because error may be from building the request object itself or from a network error */ + response?: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> +) => RequestResult; + +type SseFn = < + TData = unknown, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'> +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + ([TData] extends [never] ? unknown : Omit); diff --git a/web/src/api/client/utils.gen.ts b/web/src/api/client/utils.gen.ts new file mode 100644 index 0000000..c84bd56 --- /dev/null +++ b/web/src/api/client/utils.gen.ts @@ -0,0 +1,291 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}): (( + queryParams: T +) => string) => { + const querySerializer = (queryParams: T): string => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if (['application/', 'audio/', 'image/', 'video/'].some(type => cleanContent.startsWith(type))) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string +): boolean => { + if (!name) { + return false; + } + if (options.headers.has(name) || options.query?.[name] || options.headers.get('Cookie')?.includes(`${name}=`)) { + return true; + } + return false; +}; + +export async function setAuthParams( + options: Pick & { + headers: Headers; + } +): Promise { + for (const auth of options.security ?? []) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +} + +export const buildUrl: Client['buildUrl'] = options => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = (...headers: Array['headers'] | undefined>): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set(key, typeof value === 'object' ? JSON.stringify(value) : (value as string)); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + /** response may be undefined due to a network error where no response object is produced */ + response: Res | undefined, + /** request may be undefined, because error may be from building the request object itself */ + request: Req | undefined, + options: Options +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {} +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/web/src/api/core/auth.gen.ts b/web/src/api/core/auth.gen.ts new file mode 100644 index 0000000..60ab7bf --- /dev/null +++ b/web/src/api/core/auth.gen.ts @@ -0,0 +1,48 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * A unique identifier for the security scheme. + * + * Defined only when there are multiple security schemes whose `Auth` + * shape would otherwise be identical. + */ + key?: string; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/web/src/api/core/bodySerializer.gen.ts b/web/src/api/core/bodySerializer.gen.ts new file mode 100644 index 0000000..e19927f --- /dev/null +++ b/web/src/api/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach(v => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach(v => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/web/src/api/core/params.gen.ts b/web/src/api/core/params.gen.ts new file mode 100644 index 0000000..0f50047 --- /dev/null +++ b/web/src/api/core/params.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +function buildKeyMap(fields: FieldsConfig, map?: KeyMap): KeyMap { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +} + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +type ParamsSlotMap = Record; + +function stripEmptySlots(params: ParamsSlotMap): void { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +} + +export function buildClientParams(args: ReadonlyArray, fields: FieldsConfig): Params { + const params: ParamsSlotMap = { + body: Object.create(null), + headers: Object.create(null), + path: Object.create(null), + query: Object.create(null), + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params as Params; +} diff --git a/web/src/api/core/pathSerializer.gen.ts b/web/src/api/core/pathSerializer.gen.ts new file mode 100644 index 0000000..1294aff --- /dev/null +++ b/web/src/api/core/pathSerializer.gen.ts @@ -0,0 +1,167 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle): '.' | ';' | ',' | '&' => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle): ',' | '|' | '%20' => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle): '.' | ';' | ',' | '&' => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}): string => { + if (!explode) { + const joinedValues = (allowReserved ? value : value.map(v => encodeURIComponent(v as string))).join( + separatorArrayNoExplode(style) + ); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map(v => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam): string => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.' + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}): string => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }) + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/web/src/api/core/queryKeySerializer.gen.ts b/web/src/api/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..e84d0e4 --- /dev/null +++ b/web/src/api/core/queryKeySerializer.gen.ts @@ -0,0 +1,111 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown): unknown | undefined => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/web/src/api/core/serverSentEvents.gen.ts b/web/src/api/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..d89a228 --- /dev/null +++ b/web/src/api/core/serverSentEvents.gen.ts @@ -0,0 +1,238 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext>; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise(resolve => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/web/src/api/core/types.gen.ts b/web/src/api/core/types.gen.ts new file mode 100644 index 0000000..2b9e67b --- /dev/null +++ b/web/src/api/core/types.gen.ts @@ -0,0 +1,86 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = 'connect' | 'delete' | 'get' | 'head' | 'options' | 'patch' | 'post' | 'put' | 'trace'; + +export type Client = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/web/src/api/core/utils.gen.ts b/web/src/api/core/utils.gen.ts new file mode 100644 index 0000000..340c8bd --- /dev/null +++ b/web/src/api/core/utils.gen.ts @@ -0,0 +1,137 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE: RegExp = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer): string => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }) + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}` + ); + continue; + } + + const replaceValue = encodeURIComponent(style === 'label' ? `.${value as string}` : (value as string)); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}): string => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}): unknown { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/web/src/api/ethereum.ts b/web/src/api/ethereum.ts deleted file mode 100644 index 98f424a..0000000 --- a/web/src/api/ethereum.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Response, - V1GetEthereumNowResponse, - V1GetEthereumSpecResponse, - EthereumSpec, -} from '@app/types/api'; -import { BASE_URL } from '@utils/environment'; - -export type Spec = { - network_name: string; - spec: EthereumSpec; -}; - -export async function fetchNow(): Promise> { - const response = await fetch(`${BASE_URL}api/v1/ethereum/now`); - - if (!response.ok) { - throw new Error('Failed to fetch ethereum now'); - } - const json = (await response.json()) as Response; - - if (!json.data?.slot) throw new Error('No slot in response'); - if (!json.data?.epoch) throw new Error('No epoch in response'); - - return { - slot: json.data?.slot, - epoch: json.data?.epoch, - }; -} - -export async function fetchSpec(): Promise> { - const response = await fetch(`${BASE_URL}api/v1/ethereum/spec`); - - if (!response.ok) { - throw new Error('Failed to fetch ethereum spec'); - } - const json = (await response.json()) as Response; - - if (!json.data?.spec) throw new Error('No spec in response'); - - return { - network_name: json.data?.network_name ?? 'unknown', - spec: json.data.spec, - }; -} diff --git a/web/src/api/frames.ts b/web/src/api/frames.ts deleted file mode 100644 index 2f22462..0000000 --- a/web/src/api/frames.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Response, V1GetFrameResponse, Frame } from '@app/types/api'; -import { ProcessedData } from '@app/types/graph'; -import { BASE_URL } from '@utils/environment'; -import { processForkChoiceData } from '@utils/graph'; - -export async function fetchFrame(id: string): Promise { - const response = await fetch(`${BASE_URL}api/v1/frames/${id}`); - - if (!response.ok) { - throw new Error('Failed to fetch snapshot data'); - } - const json = (await response.json()) as Response; - const frame = json.data?.frame; - - if (frame === undefined) throw new Error('No frame in response'); - if (frame?.data === undefined) throw new Error('No frame data in response'); - if (frame?.metadata === undefined) throw new Error('No frame metadata in response'); - - return processForkChoiceData(frame as Required); -} diff --git a/web/src/api/index.ts b/web/src/api/index.ts new file mode 100644 index 0000000..96fbcb0 --- /dev/null +++ b/web/src/api/index.ts @@ -0,0 +1,75 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { + getEthereumNow, + getEthereumSpec, + getFrame, + listMetadata, + listMetadataEpochs, + listMetadataLabels, + listMetadataNodes, + listMetadataSlots, + type Options, +} from './sdk.gen'; +export type { + Checkpoint, + ClientOptions, + Epoch, + EpochString, + Error, + EthereumNow, + EthereumSpec, + EthereumSpecResult, + ForkChoice, + ForkChoiceNode, + Frame, + FrameFilter, + FrameMetadata, + GetEthereumNowData, + GetEthereumNowError, + GetEthereumNowErrors, + GetEthereumNowResponse, + GetEthereumNowResponses, + GetEthereumSpecData, + GetEthereumSpecError, + GetEthereumSpecErrors, + GetEthereumSpecResponse, + GetEthereumSpecResponses, + GetFrameData, + GetFrameError, + GetFrameErrors, + GetFrameResponse, + GetFrameResponses, + ListMetadataData, + ListMetadataEpochsData, + ListMetadataEpochsError, + ListMetadataEpochsErrors, + ListMetadataEpochsResponse, + ListMetadataEpochsResponses, + ListMetadataError, + ListMetadataErrors, + ListMetadataLabelsData, + ListMetadataLabelsError, + ListMetadataLabelsErrors, + ListMetadataLabelsResponse, + ListMetadataLabelsResponses, + ListMetadataNodesData, + ListMetadataNodesError, + ListMetadataNodesErrors, + ListMetadataNodesResponse, + ListMetadataNodesResponses, + ListMetadataResponse, + ListMetadataResponses, + ListMetadataSlotsData, + ListMetadataSlotsError, + ListMetadataSlotsErrors, + ListMetadataSlotsResponse, + ListMetadataSlotsResponses, + MetadataQuery, + MetadataQuery2, + PaginationCursor, + PaginationResponse, + Root, + Slot, + SlotString, +} from './types.gen'; diff --git a/web/src/api/metadata.ts b/web/src/api/metadata.ts deleted file mode 100644 index d52fbcc..0000000 --- a/web/src/api/metadata.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - Response, - FrameMetaData, - V1MetadataListRequest, - V1MetadataListResponse, - V1MetadataListNodesRequest, - V1MetadataListNodesResponse, -} from '@app/types/api'; -import { BASE_URL } from '@utils/environment'; - -export async function fetchMetadataNodes(payload: V1MetadataListNodesRequest): Promise { - const response = await fetch(`${BASE_URL}api/v1/metadata/nodes`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error('Failed to fetch metadata nodes'); - } - - const json = (await response.json()) as Response; - return json.data?.nodes || []; -} - -export async function fetchMetadataList(payload: V1MetadataListRequest): Promise { - const response = await fetch(`${BASE_URL}api/v1/metadata`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error('Failed to fetch metadata list'); - } - const json = (await response.json()) as Response; - return json.data?.frames || []; -} diff --git a/web/src/api/sdk.gen.ts b/web/src/api/sdk.gen.ts new file mode 100644 index 0000000..84ba864 --- /dev/null +++ b/web/src/api/sdk.gen.ts @@ -0,0 +1,174 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as Options2, RequestResult, TDataShape } from './client'; +import { client } from './client.gen'; +import type { + GetEthereumNowData, + GetEthereumNowErrors, + GetEthereumNowResponses, + GetEthereumSpecData, + GetEthereumSpecErrors, + GetEthereumSpecResponses, + GetFrameData, + GetFrameErrors, + GetFrameResponses, + ListMetadataData, + ListMetadataEpochsData, + ListMetadataEpochsErrors, + ListMetadataEpochsResponses, + ListMetadataErrors, + ListMetadataLabelsData, + ListMetadataLabelsErrors, + ListMetadataLabelsResponses, + ListMetadataNodesData, + ListMetadataNodesErrors, + ListMetadataNodesResponses, + ListMetadataResponses, + ListMetadataSlotsData, + ListMetadataSlotsErrors, + ListMetadataSlotsResponses, +} from './types.gen'; +import { + zGetEthereumNowResponse, + zGetEthereumSpecResponse, + zGetFrameResponse, + zListMetadataEpochsResponse, + zListMetadataLabelsResponse, + zListMetadataNodesResponse, + zListMetadataResponse, + zListMetadataSlotsResponse, +} from './zod.gen'; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, +> = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Get the current wall-clock slot and epoch + */ +export const getEthereumNow = ( + options?: Options +): RequestResult => + (options?.client ?? client).get({ + responseValidator: async data => await zGetEthereumNowResponse.parseAsync(data), + url: '/api/v1/ethereum/now', + ...options, + }); + +/** + * Get the Ethereum network name and spec + */ +export const getEthereumSpec = ( + options?: Options +): RequestResult => + (options?.client ?? client).get({ + responseValidator: async data => await zGetEthereumSpecResponse.parseAsync(data), + url: '/api/v1/ethereum/spec', + ...options, + }); + +/** + * Get a single fork-choice frame by ID + */ +export const getFrame = ( + options: Options +): RequestResult => + (options.client ?? client).get({ + responseValidator: async data => await zGetFrameResponse.parseAsync(data), + url: '/api/v1/frames/{id}', + ...options, + }); + +/** + * List fork-choice frame metadata + */ +export const listMetadata = ( + options: Options +): RequestResult => + (options.client ?? client).post({ + responseValidator: async data => await zListMetadataResponse.parseAsync(data), + url: '/api/v1/metadata', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * List distinct nodes across stored frames + */ +export const listMetadataNodes = ( + options: Options +): RequestResult => + (options.client ?? client).post({ + responseValidator: async data => await zListMetadataNodesResponse.parseAsync(data), + url: '/api/v1/metadata/nodes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * List distinct slots across stored frames + */ +export const listMetadataSlots = ( + options: Options +): RequestResult => + (options.client ?? client).post({ + responseValidator: async data => await zListMetadataSlotsResponse.parseAsync(data), + url: '/api/v1/metadata/slots', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * List distinct epochs across stored frames + */ +export const listMetadataEpochs = ( + options: Options +): RequestResult => + (options.client ?? client).post({ + responseValidator: async data => await zListMetadataEpochsResponse.parseAsync(data), + url: '/api/v1/metadata/epochs', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * List distinct labels across stored frames + */ +export const listMetadataLabels = ( + options: Options +): RequestResult => + (options.client ?? client).post({ + responseValidator: async data => await zListMetadataLabelsResponse.parseAsync(data), + url: '/api/v1/metadata/labels', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); diff --git a/web/src/api/types.gen.ts b/web/src/api/types.gen.ts new file mode 100644 index 0000000..2456691 --- /dev/null +++ b/web/src/api/types.gen.ts @@ -0,0 +1,452 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: 'https://forky.mainnet.ethpandaops.io' | 'https://forky.hoodi.ethpandaops.io' | (string & {}); +}; + +/** + * A beacon-chain slot number. + */ +export type Slot = number; + +/** + * A beacon-chain epoch number. + */ +export type Epoch = number; + +/** + * A beacon-chain slot number serialized as a string, following the + * beacon-API convention used by the fork-choice dump. + * + */ +export type SlotString = string; + +/** + * A beacon-chain epoch number serialized as a string, following the + * beacon-API convention used by the fork-choice dump. + * + */ +export type EpochString = string; + +/** + * A 32-byte hex-encoded root. + */ +export type Root = string; + +export type Error = { + /** + * The HTTP status code. + */ + code: number; + /** + * A human-readable error message. + */ + message: string; +}; + +/** + * Offset-based pagination cursor. + */ +export type PaginationCursor = { + /** + * The number of items to skip. + */ + offset?: number; + /** + * The maximum number of items to return. + */ + limit?: number; +}; + +export type PaginationResponse = { + /** + * The total number of matching items. + */ + total: number; +}; + +/** + * Filters applied to a frame query. All fields are optional. + */ +export type FrameFilter = { + node?: string; + before?: string; + after?: string; + slot?: Slot; + epoch?: Epoch; + labels?: Array; + consensus_client?: string; + event_source?: string; +}; + +export type MetadataQuery = { + filter?: FrameFilter; + pagination?: PaginationCursor; +}; + +export type FrameMetadata = { + /** + * The frame ID. + */ + id: string; + /** + * The node that provided the frame. For a beacon node this is the + * configured node ID; for Xatu it is the Xatu Sentry ID. + * + */ + node: string; + /** + * When the frame was fetched. + */ + fetched_at: string; + wall_clock_slot: Slot; + wall_clock_epoch: Epoch; + labels: Array; + consensus_client: string; + event_source: string; +}; + +export type Checkpoint = { + epoch: EpochString; + root: Root; +}; + +export type ForkChoiceNode = { + slot: SlotString; + block_root: Root; + parent_root: Root; + justified_epoch: EpochString; + finalized_epoch: EpochString; + /** + * The node's fork-choice weight, serialized as a string. + */ + weight: string; + /** + * The node's validity status. + */ + validity: 'valid' | 'invalid' | 'optimistic' | 'unknown'; + execution_block_hash: Root; + extra_data?: { + [key: string]: unknown; + }; +}; + +/** + * A beacon node's fork-choice dump. + */ +export type ForkChoice = { + justified_checkpoint: Checkpoint; + finalized_checkpoint: Checkpoint; + fork_choice_nodes: Array; +}; + +export type Frame = { + data: ForkChoice; + metadata: FrameMetadata; +}; + +export type EthereumNow = { + slot: Slot; + epoch: Epoch; +}; + +export type EthereumSpec = { + seconds_per_slot: number; + slots_per_epoch: number; + genesis_time: string; +}; + +export type EthereumSpecResult = { + network_name: string; + spec: EthereumSpec; +}; + +/** + * Filter and pagination for a metadata query. + */ +export type MetadataQuery2 = MetadataQuery; + +export type GetEthereumNowData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/ethereum/now'; +}; + +export type GetEthereumNowErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type GetEthereumNowError = GetEthereumNowErrors[keyof GetEthereumNowErrors]; + +export type GetEthereumNowResponses = { + /** + * The current wall-clock slot and epoch. + */ + 200: { + data: EthereumNow; + }; +}; + +export type GetEthereumNowResponse = GetEthereumNowResponses[keyof GetEthereumNowResponses]; + +export type GetEthereumSpecData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/ethereum/spec'; +}; + +export type GetEthereumSpecErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type GetEthereumSpecError = GetEthereumSpecErrors[keyof GetEthereumSpecErrors]; + +export type GetEthereumSpecResponses = { + /** + * The configured network name and spec. + */ + 200: { + data: EthereumSpecResult; + }; +}; + +export type GetEthereumSpecResponse = GetEthereumSpecResponses[keyof GetEthereumSpecResponses]; + +export type GetFrameData = { + body?: never; + path: { + /** + * The frame ID. + */ + id: string; + }; + query?: never; + url: '/api/v1/frames/{id}'; +}; + +export type GetFrameErrors = { + /** + * The requested resource was not found. + */ + 404: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type GetFrameError = GetFrameErrors[keyof GetFrameErrors]; + +export type GetFrameResponses = { + /** + * The requested fork-choice frame. + */ + 200: { + data: { + frame: Frame; + }; + }; +}; + +export type GetFrameResponse = GetFrameResponses[keyof GetFrameResponses]; + +export type ListMetadataData = { + /** + * Filter and pagination for a metadata query. + */ + body: MetadataQuery2; + path?: never; + query?: never; + url: '/api/v1/metadata'; +}; + +export type ListMetadataErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type ListMetadataError = ListMetadataErrors[keyof ListMetadataErrors]; + +export type ListMetadataResponses = { + /** + * A page of frame metadata. + */ + 200: { + data: { + frames: Array; + pagination: PaginationResponse; + }; + }; +}; + +export type ListMetadataResponse = ListMetadataResponses[keyof ListMetadataResponses]; + +export type ListMetadataNodesData = { + /** + * Filter and pagination for a metadata query. + */ + body: MetadataQuery2; + path?: never; + query?: never; + url: '/api/v1/metadata/nodes'; +}; + +export type ListMetadataNodesErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type ListMetadataNodesError = ListMetadataNodesErrors[keyof ListMetadataNodesErrors]; + +export type ListMetadataNodesResponses = { + /** + * A page of distinct node names. + */ + 200: { + data: { + nodes: Array; + pagination: PaginationResponse; + }; + }; +}; + +export type ListMetadataNodesResponse = ListMetadataNodesResponses[keyof ListMetadataNodesResponses]; + +export type ListMetadataSlotsData = { + /** + * Filter and pagination for a metadata query. + */ + body: MetadataQuery2; + path?: never; + query?: never; + url: '/api/v1/metadata/slots'; +}; + +export type ListMetadataSlotsErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type ListMetadataSlotsError = ListMetadataSlotsErrors[keyof ListMetadataSlotsErrors]; + +export type ListMetadataSlotsResponses = { + /** + * A page of distinct slots. + */ + 200: { + data: { + slots: Array; + pagination: PaginationResponse; + }; + }; +}; + +export type ListMetadataSlotsResponse = ListMetadataSlotsResponses[keyof ListMetadataSlotsResponses]; + +export type ListMetadataEpochsData = { + /** + * Filter and pagination for a metadata query. + */ + body: MetadataQuery2; + path?: never; + query?: never; + url: '/api/v1/metadata/epochs'; +}; + +export type ListMetadataEpochsErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type ListMetadataEpochsError = ListMetadataEpochsErrors[keyof ListMetadataEpochsErrors]; + +export type ListMetadataEpochsResponses = { + /** + * A page of distinct epochs. + */ + 200: { + data: { + epochs: Array; + pagination: PaginationResponse; + }; + }; +}; + +export type ListMetadataEpochsResponse = ListMetadataEpochsResponses[keyof ListMetadataEpochsResponses]; + +export type ListMetadataLabelsData = { + /** + * Filter and pagination for a metadata query. + */ + body: MetadataQuery2; + path?: never; + query?: never; + url: '/api/v1/metadata/labels'; +}; + +export type ListMetadataLabelsErrors = { + /** + * The request was malformed. + */ + 400: Error; + /** + * An unexpected error occurred. + */ + default: Error; +}; + +export type ListMetadataLabelsError = ListMetadataLabelsErrors[keyof ListMetadataLabelsErrors]; + +export type ListMetadataLabelsResponses = { + /** + * A page of distinct labels. + */ + 200: { + data: { + labels: Array; + pagination: PaginationResponse; + }; + }; +}; + +export type ListMetadataLabelsResponse = ListMetadataLabelsResponses[keyof ListMetadataLabelsResponses]; diff --git a/web/src/api/zod.gen.ts b/web/src/api/zod.gen.ts new file mode 100644 index 0000000..62eb0c8 --- /dev/null +++ b/web/src/api/zod.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +/** + * A beacon-chain slot number. + */ +export const zSlot = z.coerce + .bigint() + .check( + z.gte(BigInt(0)), + z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) + ); + +/** + * A beacon-chain epoch number. + */ +export const zEpoch = z.coerce + .bigint() + .check( + z.gte(BigInt(0)), + z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) + ); + +/** + * A beacon-chain slot number serialized as a string, following the + * beacon-API convention used by the fork-choice dump. + * + */ +export const zSlotString = z.string(); + +/** + * A beacon-chain epoch number serialized as a string, following the + * beacon-API convention used by the fork-choice dump. + * + */ +export const zEpochString = z.string(); + +/** + * A 32-byte hex-encoded root. + */ +export const zRoot = z.string(); + +export const zError = z.object({ + code: z.int(), + message: z.string(), +}); + +/** + * Offset-based pagination cursor. + */ +export const zPaginationCursor = z.object({ + offset: z._default(z.optional(z.int()), 0), + limit: z._default(z.optional(z.int()), 1000), +}); + +export const zPaginationResponse = z.object({ + total: z.coerce + .bigint() + .check( + z.minimum(BigInt('-9223372036854775808'), { + error: 'Invalid value: Expected int64 to be >= -9223372036854775808', + }), + z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) + ), +}); + +/** + * Filters applied to a frame query. All fields are optional. + */ +export const zFrameFilter = z.object({ + node: z.optional(z.string()), + before: z.optional(z.iso.datetime()), + after: z.optional(z.iso.datetime()), + slot: z.optional(zSlot), + epoch: z.optional(zEpoch), + labels: z.optional(z.array(z.string())), + consensus_client: z.optional(z.string()), + event_source: z.optional(z.string()), +}); + +export const zMetadataQuery = z.object({ + filter: z.optional(zFrameFilter), + pagination: z.optional(zPaginationCursor), +}); + +export const zFrameMetadata = z.object({ + id: z.string(), + node: z.string(), + fetched_at: z.iso.datetime(), + wall_clock_slot: zSlot, + wall_clock_epoch: zEpoch, + labels: z.array(z.string()), + consensus_client: z.string(), + event_source: z.string(), +}); + +export const zCheckpoint = z.object({ + epoch: zEpochString, + root: zRoot, +}); + +export const zForkChoiceNode = z.object({ + slot: zSlotString, + block_root: zRoot, + parent_root: zRoot, + justified_epoch: zEpochString, + finalized_epoch: zEpochString, + weight: z.string(), + validity: z.enum(['valid', 'invalid', 'optimistic', 'unknown']), + execution_block_hash: zRoot, + extra_data: z.optional(z.record(z.string(), z.unknown())), +}); + +/** + * A beacon node's fork-choice dump. + */ +export const zForkChoice = z.object({ + justified_checkpoint: zCheckpoint, + finalized_checkpoint: zCheckpoint, + fork_choice_nodes: z.array(zForkChoiceNode), +}); + +export const zFrame = z.object({ + data: zForkChoice, + metadata: zFrameMetadata, +}); + +export const zEthereumNow = z.object({ + slot: zSlot, + epoch: zEpoch, +}); + +export const zEthereumSpec = z.object({ + seconds_per_slot: z.coerce + .bigint() + .check( + z.minimum(BigInt('-9223372036854775808'), { + error: 'Invalid value: Expected int64 to be >= -9223372036854775808', + }), + z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) + ), + slots_per_epoch: z.coerce + .bigint() + .check( + z.minimum(BigInt('-9223372036854775808'), { + error: 'Invalid value: Expected int64 to be >= -9223372036854775808', + }), + z.maximum(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }) + ), + genesis_time: z.iso.datetime(), +}); + +export const zEthereumSpecResult = z.object({ + network_name: z.string(), + spec: zEthereumSpec, +}); + +/** + * Filter and pagination for a metadata query. + */ +export const zMetadataQuery2 = zMetadataQuery; + +/** + * The current wall-clock slot and epoch. + */ +export const zGetEthereumNowResponse = z.object({ + data: zEthereumNow, +}); + +/** + * The configured network name and spec. + */ +export const zGetEthereumSpecResponse = z.object({ + data: zEthereumSpecResult, +}); + +export const zGetFramePath = z.object({ + id: z.string(), +}); + +/** + * The requested fork-choice frame. + */ +export const zGetFrameResponse = z.object({ + data: z.object({ + frame: zFrame, + }), +}); + +/** + * Filter and pagination for a metadata query. + */ +export const zListMetadataBody = zMetadataQuery2; + +/** + * A page of frame metadata. + */ +export const zListMetadataResponse = z.object({ + data: z.object({ + frames: z.array(zFrameMetadata), + pagination: zPaginationResponse, + }), +}); + +/** + * Filter and pagination for a metadata query. + */ +export const zListMetadataNodesBody = zMetadataQuery2; + +/** + * A page of distinct node names. + */ +export const zListMetadataNodesResponse = z.object({ + data: z.object({ + nodes: z.array(z.string()), + pagination: zPaginationResponse, + }), +}); + +/** + * Filter and pagination for a metadata query. + */ +export const zListMetadataSlotsBody = zMetadataQuery2; + +/** + * A page of distinct slots. + */ +export const zListMetadataSlotsResponse = z.object({ + data: z.object({ + slots: z.array(zSlot), + pagination: zPaginationResponse, + }), +}); + +/** + * Filter and pagination for a metadata query. + */ +export const zListMetadataEpochsBody = zMetadataQuery2; + +/** + * A page of distinct epochs. + */ +export const zListMetadataEpochsResponse = z.object({ + data: z.object({ + epochs: z.array(zEpoch), + pagination: zPaginationResponse, + }), +}); + +/** + * Filter and pagination for a metadata query. + */ +export const zListMetadataLabelsBody = zMetadataQuery2; + +/** + * A page of distinct labels. + */ +export const zListMetadataLabelsResponse = z.object({ + data: z.object({ + labels: z.array(z.string()), + pagination: zPaginationResponse, + }), +}); diff --git a/web/src/assets/forky-mark-small.svg b/web/src/assets/forky-mark-small.svg new file mode 100644 index 0000000..3cafc98 --- /dev/null +++ b/web/src/assets/forky-mark-small.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/web/src/assets/forky-mark.svg b/web/src/assets/forky-mark.svg new file mode 100644 index 0000000..1ccd5a4 --- /dev/null +++ b/web/src/assets/forky-mark.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/assets/forky_logo.png b/web/src/assets/forky_logo.png index 0c77364..dba1e93 100644 Binary files a/web/src/assets/forky_logo.png and b/web/src/assets/forky_logo.png differ diff --git a/web/src/assets/forky_logo_small.png b/web/src/assets/forky_logo_small.png index ecfd1bf..a604849 100644 Binary files a/web/src/assets/forky_logo_small.png and b/web/src/assets/forky_logo_small.png differ diff --git a/web/src/components/AggregatedBlockSummary.tsx b/web/src/components/AggregatedBlockSummary.tsx index dce6230..78307c7 100644 --- a/web/src/components/AggregatedBlockSummary.tsx +++ b/web/src/components/AggregatedBlockSummary.tsx @@ -1,20 +1,18 @@ -import { - CheckCircleIcon, - MagnifyingGlassIcon, - XCircleIcon, - ArrowTopRightOnSquareIcon, -} from '@heroicons/react/20/solid'; +import { CheckCircleIcon, MagnifyingGlassIcon, XCircleIcon } from '@heroicons/react/20/solid'; import classNames from 'clsx'; -import { Link } from 'wouter'; +import Link from '@components/Link'; import { ProcessedData, WeightedNodeAttributes } from '@app/types/graph'; -import { truncateHash } from '@app/utils/strings'; import Loading from '@components/Loading'; +import { SummaryCard, SummaryLink, SummaryRow, SummarySection } from '@components/Summary'; import useEthereum from '@contexts/ethereum'; import useSelection, { AggregatedFramesBlock } from '@contexts/selection'; import { useFrameQueries } from '@hooks/useQuery'; import { aggregateProcessedData } from '@utils/graph'; +const HEADER_CELL = 'px-3 py-2 text-center text-xs font-semibold text-foreground'; +const ICON_CELL = 'whitespace-nowrap py-2 text-sm text-muted'; + export default function AggregatedBlockSummary({ frameIds, blockRoot }: AggregatedFramesBlock) { const { slotsPerEpoch } = useEthereum(); const { clearAll, setFrameBlock } = useSelection(); @@ -48,238 +46,197 @@ export default function AggregatedBlockSummary({ frameIds, blockRoot }: Aggregat ); return ( -
-
-
-
-
Epoch
-
- {Math.floor(node.slot / slotsPerEpoch)} -
-
-
-
Slot
-
- {node.slot} -
-
-
-
Block Root
-
- - - {truncateHash(node.blockRoot)} - - - - {node.blockRoot} -
-
-
-
Status
-
-
- - - - - - + + {Math.floor(node.slot / slotsPerEpoch)} + {node.slot} + + {node.blockRoot} + + +
+
- Source - - Canonical - - Seen -
+ + + + + + {hasFinalized && ( + + )} + {hasJustified && ( + + )} + {hasInvalid && ( + + )} + {hasOrphaned && ( + + )} + + + + + {results.map(({ data }) => { + if (!data) return null; + let isAggregatedCanonical = false; + let seen = false; + let canonical = false; + let validity = 'valid'; + let orphaned = false; + let attributes: WeightedNodeAttributes | undefined; + try { + const aggregatedAttributes = graph.getNodeAttributes(nodeId); + isAggregatedCanonical = aggregatedAttributes.canonical; + attributes = data.graph.getNodeAttributes(nodeId); + validity = attributes.validity; + orphaned = attributes.orphaned || false; + seen = true; + canonical = attributes.canonical; + } catch (err) { + // ignore + } + return ( + + + + {hasFinalized && ( - + )} {hasJustified && ( - + )} {hasInvalid && ( - + )} {hasOrphaned && ( - + )} - - - - - {results.map(({ data }) => { - if (!data) return null; - let isAggregatedCanonical = false; - let seen = false; - let canonical = false; - let validity = 'valid'; - let orphaned = false; - let attributes: WeightedNodeAttributes | undefined; - try { - const aggregatedAttributes = graph.getNodeAttributes(nodeId); - isAggregatedCanonical = aggregatedAttributes.canonical; - attributes = data.graph.getNodeAttributes(nodeId); - validity = attributes.validity; - orphaned = attributes.orphaned || false; - seen = true; - canonical = attributes.canonical; - } catch (err) { - // ignore - } - return ( - - - - - {hasFinalized && ( - - )} - {hasJustified && ( - - )} - {hasInvalid && ( - - )} - {hasOrphaned && ( - - )} - - - ); - })} - -
+ Source + + Canonical + + Seen + + Finalized + + Justified + + Validity + + Detached + + View +
+ + {data.frame.metadata.node} + + +
+ {seen && + (canonical ? ( + + ) : ( + + ))} +
+
+
+ {seen ? ( + + ) : ( + + )} +
+
- Finalized - +
+ {attributes?.checkpoint === 'finalized' ? ( + + ) : ( + + )} +
+
- Justified - +
+ {attributes?.checkpoint === 'justified' ? ( + + ) : ( + + )} +
+
- Validity - +
+ {validity.toLowerCase() === 'valid' ? ( + + ) : ( + + )} +
+
- Detached - +
+ {orphaned ? ( + + ) : ( + + )} +
+
- View -
- - {data.frame.metadata.node} + + {attributes && ( +
+ + setFrameBlock({ blockRoot, frameId: data.frame.metadata.id }) + } + > + - -
-
- {seen && - (canonical ? ( - - ) : ( - - ))} -
-
-
- {seen ? ( - - ) : ( - - )} -
-
-
- {attributes?.checkpoint === 'finalized' ? ( - - ) : ( - - )} -
-
-
- {attributes?.checkpoint === 'justified' ? ( - - ) : ( - - )} -
-
-
- {validity.toLowerCase() === 'valid' ? ( - - ) : ( - - )} -
-
-
- {orphaned ? ( - - ) : ( - - )} -
-
- {attributes && ( -
- - setFrameBlock({ blockRoot, frameId: data.frame.metadata.id }) - } - > - - -
- )} -
-
-
+
+ )} + + + ); + })} + +
-
+ ); } diff --git a/web/src/components/AggregatedFramesSummary.tsx b/web/src/components/AggregatedFramesSummary.tsx index 40923e5..eaa7836 100644 --- a/web/src/components/AggregatedFramesSummary.tsx +++ b/web/src/components/AggregatedFramesSummary.tsx @@ -1,14 +1,20 @@ -import { MagnifyingGlassIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'; +import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'; import classNames from 'clsx'; import ReactTimeAgo from 'react-time-ago'; -import { Link } from 'wouter'; +import Link from '@components/Link'; import { ProcessedData, WeightedNodeAttributes } from '@app/types/graph'; import Download from '@components/Download'; import Loading from '@components/Loading'; +import { SummaryLink } from '@components/Summary'; import useSelection from '@contexts/selection'; import { useFrameQueries } from '@hooks/useQuery'; import { aggregateProcessedData } from '@utils/graph'; +import { truncateHash } from '@utils/strings'; + +const HEADER_CELL = + 'px-2.5 py-2 text-left font-mono text-[10px] font-semibold uppercase tracking-wider text-faint'; +const BODY_CELL = 'whitespace-nowrap px-2.5 py-2 text-sm text-foreground'; export default function AggregatedFramesSummary({ ids }: { ids: string[] }) { const { clearAll } = useSelection(); @@ -25,206 +31,153 @@ export default function AggregatedFramesSummary({ ids }: { ids: string[] }) { ); return ( -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - {results.map(({ data }, i) => { - if (!data) - return ( - - - - ); - let head: WeightedNodeAttributes | undefined; - try { - head = data.graph.getNodeAttributes(data.graph.getAttribute('head')); - } catch (err) { - // ignore - } +
+
+
- Source - - Taken At - - Head - - Head Root - - Finalized - - Justified - - Snapshot ID - - View -
- Slot - - Root - - Epoch - - Root - - Epoch - - Root -
Data error
+ + + + + + + + + + + + + + + + + + + + + {results.map(({ data }, i) => { + if (!data) + return ( + + + + ); + let head: WeightedNodeAttributes | undefined; + try { + head = data.graph.getNodeAttributes(data.graph.getAttribute('head')); + } catch (err) { + // ignore + } - const isCanonicalHead = - graph.getAttribute('head') === data.graph.getAttribute('head'); - return ( - - - - - - - - - - - - - ); - })} - -
+ Source + + Taken At + + Head + + Finalized + + Justified + + Snapshot ID + + View +
+ Slot + + Root + + Epoch + + Root + + Epoch + + Root +
+ Data error +
- - {data.frame.metadata.node} - - - - - - ({new Date(data.frame.metadata.fetched_at).toISOString()}) - - - {head?.slot} - - {head?.blockRoot} - - {data.frame.data.finalized_checkpoint?.epoch} - - {data.frame.data.finalized_checkpoint?.root} - - {data.frame.data.justified_checkpoint?.epoch} - - {data.frame.data.justified_checkpoint?.root} - - - {data.frame.metadata.id} - - - - - - - -
-
-
-
-
+ const isCanonicalHead = + graph.getAttribute('head') === data.graph.getAttribute('head'); + return ( + + + + {data.frame.metadata.node} + + + + + + + {head?.slot} + + + {head?.blockRoot && truncateHash(head.blockRoot)} + + {data.frame.data.finalized_checkpoint?.epoch} + + {data.frame.data.finalized_checkpoint?.root && + truncateHash(data.frame.data.finalized_checkpoint.root)} + + {data.frame.data.justified_checkpoint?.epoch} + + {data.frame.data.justified_checkpoint?.root && + truncateHash(data.frame.data.justified_checkpoint.root)} + + + + {truncateHash(data.frame.metadata.id)} + + + + + + + + + + ); + })} + +
); diff --git a/web/src/components/AggregatedNode.tsx b/web/src/components/AggregatedNode.tsx index 9fc7dd4..ff263bf 100644 --- a/web/src/components/AggregatedNode.tsx +++ b/web/src/components/AggregatedNode.tsx @@ -1,10 +1,59 @@ -import { useState, memo } from 'react'; +import { memo } from 'react'; import { EyeIcon, CheckIcon, FlagIcon } from '@heroicons/react/20/solid'; import classNames from 'clsx'; import ProgressCircle from '@components/ProgressCircle'; +/* Mirrors the WeightedNode status recipe: dim core, bright rim, soft glow. */ +const NODE_STYLES: Record< + string, + { fill: string; arc: string; track: string; label: string; glow: string } +> = { + canonical: { + fill: 'bg-canonical-deep', + arc: 'text-canonical-ring', + track: 'text-canonical-edge', + label: 'text-canonical', + glow: 'shadow-[0_0_90px_-15px_var(--color-canonical-ring)]', + }, + fork: { + fill: 'bg-fork-deep', + arc: 'text-fork-ring', + track: 'text-fork-edge', + label: 'text-fork', + glow: 'shadow-[0_0_90px_-15px_var(--color-fork-ring)]', + }, + finalized: { + fill: 'bg-finalized-deep', + arc: 'text-finalized-ring', + track: 'text-finalized-edge', + label: 'text-finalized', + glow: 'shadow-[0_0_90px_-15px_var(--color-finalized-ring)]', + }, + justified: { + fill: 'bg-justified-deep', + arc: 'text-justified-ring', + track: 'text-justified-edge', + label: 'text-justified', + glow: 'shadow-[0_0_90px_-15px_var(--color-justified-ring)]', + }, + invalid: { + fill: 'bg-invalid-deep', + arc: 'text-invalid-ring', + track: 'text-invalid-edge', + label: 'text-invalid', + glow: 'shadow-[0_0_90px_-15px_var(--color-invalid-ring)]', + }, + optimistic: { + fill: 'bg-optimistic-deep', + arc: 'text-optimistic-ring', + track: 'text-optimistic-edge', + label: 'text-optimistic', + glow: 'shadow-[0_0_90px_-15px_var(--color-optimistic-ring)]', + }, +}; + function AggregatedNode({ id, hash, @@ -40,85 +89,31 @@ function AggregatedNode({ className?: string; onClick?: (hash: string) => void; }) { - const [isHighlighted, setIsHighlighted] = useState(false); - - const [color, backgroundColor, borderColor, title] = (() => { - if (valid !== seen) { - return [ - 'text-red-600', - 'text-red-800', - 'border-red-500 dark:border-red-900', - `${seen - valid} NOT VALID`, - ]; - } - - if (orphans > 0) { - return [ - 'text-red-600', - 'text-red-800', - 'border-red-500 dark:border-red-900', - `${orphans} DETACHED`, - ]; - } - - if (finalizedCheckpoints > 0) { - return [ - 'text-fuchsia-600', - 'text-fuchsia-800', - 'border-fuchsia-500 dark:border-fuchsia-900', - 'FINALIZED', - ]; - } - - if (justifiedCheckpoints > 0) { - return [ - 'text-indigo-600', - 'text-indigo-800', - 'border-indigo-500 dark:border-indigo-900', - 'JUSTIFIED', - ]; - } + const [styleKey, title] = ((): [string, string] => { + if (valid !== seen) return ['invalid', `${seen - valid} NOT VALID`]; + if (orphans > 0) return ['invalid', `${orphans} DETACHED`]; + if (finalizedCheckpoints > 0) return ['finalized', 'FINALIZED']; + if (justifiedCheckpoints > 0) return ['justified', 'JUSTIFIED']; switch (type) { case 'canonical': - if (optimistic > 0) { - return [ - 'text-yellow-600', - 'text-yellow-800', - 'border-yellow-500 dark:border-yellow-900', - `${optimistic}/${valid} OPTIMISTIC`, - ]; - } - return [ - 'text-emerald-600', - 'text-emerald-800', - 'border-emerald-500 dark:border-emerald-900', - 'VALID', - ]; + if (optimistic > 0) return ['optimistic', `${optimistic}/${valid} OPTIMISTIC`]; + return ['canonical', 'VALID']; case 'fork': - return [ - 'text-amber-600', - 'text-amber-800', - 'border-amber-500 dark:border-amber-900', - optimistic > 0 ? `${optimistic}/${valid} OPTIMISTIC` : 'VALID', - ]; + return ['fork', optimistic > 0 ? `${optimistic}/${valid} OPTIMISTIC` : 'VALID']; default: - return [ - 'text-emerald-600', - 'text-emerald-800', - 'border-emerald-500 dark:border-emerald-900', - type, - ]; + return ['canonical', type]; } })(); + const styles = NODE_STYLES[styleKey] ?? NODE_STYLES.canonical; return (
onClick?.(hash)} - onMouseEnter={() => setIsHighlighted(true)} - onMouseLeave={() => setIsHighlighted(false)} > -

{title}

-

- {hash.substring(0, 6)}...{hash.substring(hash.length - 4)} +

+ {title} +

+

+ {hash.substring(0, 6)}…{hash.substring(hash.length - 4)}

-

+

{finalizedCheckpoints > 0 || justifiedCheckpoints > 0 ? ( <> - {finalizedCheckpoints || justifiedCheckpoints}/ - {total} + {finalizedCheckpoints || justifiedCheckpoints}/{total} ) : ( <> - {canonical}/{total} + {canonical}/{total} )} - {seen}/{total} + {seen}/{total}

diff --git a/web/src/components/BYOFrameBlockSummary.tsx b/web/src/components/BYOFrameBlockSummary.tsx index 87ec687..b1a56f6 100644 --- a/web/src/components/BYOFrameBlockSummary.tsx +++ b/web/src/components/BYOFrameBlockSummary.tsx @@ -2,10 +2,10 @@ import ReactTimeAgo from 'react-time-ago'; import Download from '@components/Download'; import Loading from '@components/Loading'; +import { SummaryCard, SummaryRow, SummarySection, ValidityBadge } from '@components/Summary'; import useEthereum from '@contexts/ethereum'; import useFocus from '@contexts/focus'; import { FrameBlock } from '@contexts/selection'; -import { truncateHash } from '@utils/strings'; export default function BYOFrameBlockSummary({ blockRoot }: FrameBlock) { const { byoData: data } = useFocus(); @@ -23,121 +23,44 @@ export default function BYOFrameBlockSummary({ blockRoot }: FrameBlock) { return ( <> -
-
-
-
-
Epoch
-
- {Math.floor(Number.parseInt(node.slot) / slotsPerEpoch)} -
-
-
-
Slot
-
- {node.slot} -
-
-
-
- Snapshot Time -
-
- - ({data.frame.metadata.fetched_at}) -
-
-
-
Block Root
-
- - - {truncateHash(node.block_root)} - - - - {node.block_root} -
-
-
-
- Parent Root -
-
- - - {truncateHash(node.parent_root)} - - - - {node.parent_root} -
-
-
-
- Execution Block Hash -
-
- - - {truncateHash(node.execution_block_hash)} - - - - {node.execution_block_hash} -
-
-
-
Weight
-
- {node.weight} -
-
-
-
Validity
-
- {node.validity} -
-
-
-
- Justified Epoch -
-
- {node.justified_epoch} -
-
-
-
- Finalized Epoch -
-
- {node.finalized_epoch} -
-
- {node.extra_data && ( - <> -
-
- Extra data -
-
- {Object.entries(node.extra_data).map(([key, value]) => ( -
-
- {key.replaceAll('_', ' ')} -
-
- {`${value}`} -
-
- ))} - - )} -
-
-
-
+ + + {Math.floor(Number.parseInt(node.slot) / slotsPerEpoch)} + + {node.slot} + + + {data.frame.metadata.fetched_at} + + + {node.block_root} + + + {node.parent_root} + + + {node.execution_block_hash} + + + {node.weight} + + + + + {node.justified_epoch} + {node.finalized_epoch} + {node.extra_data && ( + <> + + {Object.entries(node.extra_data).map(([key, value]) => ( + + {`${value}`} + + ))} + + )} + +
{ + if (new URLSearchParams(window.location.search).get('t')) { + navigate(pathname, { replace: true }); + } + }, [pathname, navigate]); + + return null; +} diff --git a/web/src/components/ConcatNode.tsx b/web/src/components/ConcatNode.tsx index 506c789..db929e3 100644 --- a/web/src/components/ConcatNode.tsx +++ b/web/src/components/ConcatNode.tsx @@ -21,9 +21,9 @@ function ConcatNode({
-

CONCAT

-

+

CONCAT

+

{slotStart} → {slotEnd}

diff --git a/web/src/components/Control.tsx b/web/src/components/Control.tsx index 5ee1f9a..363e93e 100644 --- a/web/src/components/Control.tsx +++ b/web/src/components/Control.tsx @@ -6,12 +6,22 @@ import { PauseIcon, PlayIcon, } from '@heroicons/react/20/solid'; +import classNames from 'clsx'; import EditableInput from '@components/EditableInput'; import useEthereum from '@contexts/ethereum'; import useFocus from '@contexts/focus'; +import useNow from '@hooks/useNow'; import useOutsideInteraction from '@hooks/useOutsideInteraction'; +const FIELD = + 'flex items-center gap-2 rounded-lg border border-border-strong bg-field px-2.5 transition-colors duration-150 focus-within:border-accent'; +const FIELD_LABEL = 'font-mono text-[9px]/3 uppercase tracking-widest text-faint'; +const FIELD_INPUT = + 'block w-full bg-transparent py-1.5 font-mono text-xs/5 tabular-nums text-foreground focus:outline-hidden'; +const STEP_BUTTON = + 'hidden size-9 shrink-0 items-center justify-center rounded-md text-muted transition-colors duration-150 hover:bg-overlay/5 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent md:flex'; + function TimelineControl() { const controlRef = useRef(null); const blurAllInputs = () => { @@ -33,9 +43,11 @@ function TimelineControl() { playing, } = useFocus(); const { genesisTime, secondsPerSlot } = useEthereum(); + const now = useNow(); - const slotDiff = Math.floor((Date.now() - genesisTime) / 1000 / secondsPerSlot) - focusedSlot; + const slotDiff = Math.floor((now - genesisTime) / 1000 / secondsPerSlot) - focusedSlot; const isCloseToLiveSlot = slotDiff <= 3 && slotDiff >= -1; + const isLive = playing && isCloseToLiveSlot; const handleBack = () => { if (focusedTimeIntoSlot > 500) { @@ -59,64 +71,45 @@ function TimelineControl() { }; return ( -
-
-
- +
+
+
-
- + +
-
- -
- setFocusedTime(value)} - type="datetime-local" - /> -
-
-
- - - +
+
+
); diff --git a/web/src/components/Controls.stories.tsx b/web/src/components/Controls.stories.tsx new file mode 100644 index 0000000..a6c9278 --- /dev/null +++ b/web/src/components/Controls.stories.tsx @@ -0,0 +1,130 @@ +import { useState } from 'react'; + +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import { ArrowDownTrayIcon, CheckCircleIcon } from '@heroicons/react/20/solid'; +import { expect, userEvent, waitFor, within } from 'storybook/test'; + +import Download from '@components/Download'; +import EditableInput from '@components/EditableInput'; +import Icon from '@components/Icon'; +import { ModeToggle } from '@components/ModeToggle'; +import Share from '@components/Share'; +import { withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Components/Controls', + parameters: { + layout: 'fullscreen', + tanstack: { + router: { + path: '/', + }, + }, + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +function EditableInputSet() { + const [textValue, setTextValue] = useState('ams3-teku-001'); + const [slotValue, setSlotValue] = useState(107); + const [timeValue, setTimeValue] = useState(new Date('2024-01-01T00:21:24.000Z').getTime()); + + return ( +
+ + + +
+ ); +} + +export const ActionsAndThemeToggle: Story = { + render: () => ( +
+
+
+ + + +
+
+ } text="Small" /> + } + text="Medium" + size="md" + /> + } + text="Large" + size="lg" + /> + +
+
+
+ ), +}; + +export const EditableInputs: Story = { + render: () => ( +
+ +
+ ), +}; + +export const EditableInputEditing: Story = { + render: () => ( +
+ +
+ ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const input = canvas.getByDisplayValue('ams3-teku-001'); + await userEvent.click(input); + await userEvent.clear(input); + await userEvent.type(input, 'syd1-lighthouse-001'); + await expect(input).toHaveValue('syd1-lighthouse-001'); + }, +}; + +export const ShareClosed: Story = { + render: () => ( +
+ +
+ ), +}; + +export const ShareDialogOpen: Story = { + render: () => ( +
+ +
+ ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(canvas.getByTitle('Share link')); + await waitFor(() => expect(canvas.getByRole('button', { name: /copy/i })).toBeVisible()); + }, +}; diff --git a/web/src/components/Download.tsx b/web/src/components/Download.tsx index 02705ff..56ec391 100644 --- a/web/src/components/Download.tsx +++ b/web/src/components/Download.tsx @@ -21,7 +21,7 @@ export default function Download({ href={`data:${type};charset=utf-8,${encodeURIComponent(data)}`} download={filename} className={classNames( - 'flex items-center rounded transition hover:bg-stone-900/5 dark:hover:bg-white/5 gap-2', + 'flex items-center rounded transition hover:bg-overlay/5 gap-2', `text-${size}`, size === 'sm' && 'p-2', size === 'md' && 'p-3', diff --git a/web/src/components/Edge.tsx b/web/src/components/Edge.tsx index 746a94f..5d46d2b 100644 --- a/web/src/components/Edge.tsx +++ b/web/src/components/Edge.tsx @@ -34,7 +34,7 @@ function Edge({ return (
); diff --git a/web/src/components/EditableInput.tsx b/web/src/components/EditableInput.tsx index b833458..247c550 100644 --- a/web/src/components/EditableInput.tsx +++ b/web/src/components/EditableInput.tsx @@ -10,14 +10,19 @@ type ValueType = { : never; }; -interface Props - extends Omit, 'value' | 'onChange' | 'type'> { +interface Props extends Omit< + HTMLAttributes, + 'value' | 'onChange' | 'type' +> { value: ValueType[T]; onChange: (newValue: ValueType[T]) => void; type: T; } -function EditableInput({ value, onChange, type, id }: Props) { +const DEFAULT_CLASSES = + 'block w-full rounded-lg border border-border-strong bg-field px-2.5 py-1.5 font-mono text-xs/5 tabular-nums text-foreground transition-colors duration-150 placeholder:text-faint focus:border-accent focus:outline-hidden'; + +function EditableInput({ value, onChange, type, id, className }: Props) { const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); @@ -49,11 +54,15 @@ function EditableInput({ value, onChange, type, id }: Props } }, [isEditing]); - useEffect(() => { + // Sync the draft with the external value outside of editing, adjusting + // state during render instead of in an effect. + const [prevSync, setPrevSync] = useState({ value, isEditing }); + if (prevSync.value !== value || prevSync.isEditing !== isEditing) { + setPrevSync({ value, isEditing }); if (!isEditing) { setInputValue(value); } - }, [value, isEditing]); + } const handleSave = () => { if (isEditing) { @@ -77,7 +86,7 @@ function EditableInput({ value, onChange, type, id }: Props id={id} ref={inputRef} type={type} - className="pl-2 bg-stone-100 dark:bg-stone-500 block w-full rounded-md border-0 py-1 text-stone-900 dark:text-stone-100 shadow-sm ring-1 ring-inset ring-stone-300 dark:ring-stone-700 placeholder:text-stone-400 focus:ring-2 focus:ring-inset focus:ring-stone-600 text-sm leading-6" + className={className ?? DEFAULT_CLASSES} value={formattedValue(inputValue)} onChange={e => setInputValue(e.target.value as unknown as ValueType[T])} onBlur={handleSave} @@ -88,7 +97,7 @@ function EditableInput({ value, onChange, type, id }: Props +
{leftSideRulers}
-
-
-
-
-
Epoch
-
- {Math.floor(Number.parseInt(node.slot) / slotsPerEpoch)} -
-
-
-
Slot
-
- {node.slot} -
-
-
-
- Snapshot Time -
-
- - ({data.frame.metadata.fetched_at}) -
-
-
-
- Snapshot ID -
-
- - {data.frame.metadata.id} - - -
-
-
-
Source
-
- - {data.frame.metadata.node} - - -
-
-
-
Block Root
-
- - - {truncateHash(node.block_root)} - - - - {node.block_root} -
-
-
-
- Parent Root -
-
- - - {truncateHash(node.parent_root)} - - - - {node.parent_root} -
-
-
-
- Execution Block Hash -
-
- - - {truncateHash(node.execution_block_hash)} - - - - {node.execution_block_hash} -
-
-
-
Weight
-
- {node.weight} -
-
-
-
Validity
-
- {node.validity} -
-
-
-
- Justified Epoch -
-
- {node.justified_epoch} -
-
-
-
- Finalized Epoch -
-
- {node.finalized_epoch} -
-
- {node.extra_data && ( - <> -
-
- Extra data -
-
- {Object.entries(node.extra_data).map(([key, value]) => ( -
-
- {key.replaceAll('_', ' ')} -
-
- {`${value}`} -
-
- ))} - - )} -
-
-
-
+ + + {Math.floor(Number.parseInt(node.slot) / slotsPerEpoch)} + + {node.slot} + + + {data.frame.metadata.fetched_at} + + + + {data.frame.metadata.id} + + + + + {data.frame.metadata.node} + + + + {node.block_root} + + + {node.parent_root} + + + {node.execution_block_hash} + + + {node.weight} + + + + + {node.justified_epoch} + {node.finalized_epoch} + {node.extra_data && ( + <> + + {Object.entries(node.extra_data).map(([key, value]) => ( + + {`${value}`} + + ))} + + )} + +
-
-
-
-
-
ID
-
- - {data.frame.metadata.id} - - -
-
-
-
Taken at
-
- - - ({new Date(data.frame.metadata.fetched_at).toISOString()}) - -
-
-
-
Source
-
- - {data.frame.metadata.node} - - -
-
-
-
- Wall clock Epoch -
-
- {data.frame.metadata.wall_clock_epoch} -
-
-
-
- Wall clock slot -
-
- {data.frame.metadata.wall_clock_slot} -
-
- {data.frame.metadata.labels && ( - <> -
-
Labels
-
- {data.frame.metadata.labels?.map(label => { - const [key, value] = parseLabel(label); - return ( -
-
- {key.replaceAll('_', ' ')} -
-
- {value} -
-
- ); - })} - - )} -
-
-
-
+ + + + {data.frame.metadata.id} + + + + + + {new Date(data.frame.metadata.fetched_at).toISOString()} + + + + + {data.frame.metadata.node} + + + {data.frame.metadata.wall_clock_epoch} + {data.frame.metadata.wall_clock_slot} + {data.frame.metadata.labels && data.frame.metadata.labels.length > 0 && ( + <> + + {data.frame.metadata.labels?.map(label => { + const [key, value] = parseLabel(label); + return ( + + {value} + + ); + })} + + )} + +
; + +export const WeightedNodeStates: Story = { + render: () => ( +
+ + + + + + + + +
+ ), +}; + +export const AggregatedNodeStates: Story = { + render: () => ( +
+ + + + + + + +
+ ), +}; + +export const EdgesBoundariesAndProgress: Story = { + render: () => ( +
+ + + + + + + + + +
+ + + +
+
+ ), +}; diff --git a/web/src/components/Icon.tsx b/web/src/components/Icon.tsx index e4f7bca..210852c 100644 --- a/web/src/components/Icon.tsx +++ b/web/src/components/Icon.tsx @@ -16,7 +16,7 @@ export default function Icon({ return ( ; + title?: string; + children?: ReactNode; +} + +/** The app's link primitive: links take plain paths and route to typed targets. */ +export default function Link({ href, ...props }: Props) { + const route = parseAppRoute(href); + + switch (route?.kind) { + case 'home': + return ; + case 'events': + return ; + case 'byo': + return ; + case 'byoEvents': + return ; + case 'node': + return ; + case 'nodeEvents': + return ; + case 'snapshot': + return ; + case 'snapshotEvents': + return ( + + ); + default: + return ; + } +} diff --git a/web/src/components/Loading.stories.tsx b/web/src/components/Loading.stories.tsx new file mode 100644 index 0000000..e7fd9e2 --- /dev/null +++ b/web/src/components/Loading.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import Loading from './Loading'; + +const meta = { + title: 'Components/Loading', + component: Loading, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + Story => ( +
+ +
+ ), + ], + args: { + message: 'Loading...', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Shell: Story = { + decorators: [ + Story => ( +
+ +
+ ), + ], + args: { + message: 'Loading fork-choice data...', + textColor: 'text-on-shell', + }, +}; diff --git a/web/src/components/Loading.tsx b/web/src/components/Loading.tsx index 37ea360..35e55c9 100644 --- a/web/src/components/Loading.tsx +++ b/web/src/components/Loading.tsx @@ -15,12 +15,12 @@ export default function Loading({
- -

{message}

+ +

{message}

); } diff --git a/web/src/components/ModeToggle.tsx b/web/src/components/ModeToggle.tsx index d28330c..53d95cf 100644 --- a/web/src/components/ModeToggle.tsx +++ b/web/src/components/ModeToggle.tsx @@ -33,12 +33,12 @@ export function ModeToggle() { return ( ); } diff --git a/web/src/components/Ruler.tsx b/web/src/components/Ruler.tsx index f11f7bd..87f9b0f 100644 --- a/web/src/components/Ruler.tsx +++ b/web/src/components/Ruler.tsx @@ -37,16 +37,16 @@ const Ruler: React.FC = ({
{markText && isCentimeter && i !== 0 && i != totalSubmarks && ( {i / subMarksInterval} @@ -63,20 +63,20 @@ const Ruler: React.FC = ({
-
{children}
+
{children}
{summary && ( - + {summary} )}
{generateRulerMarks()}
diff --git a/web/src/components/SelectionSummaries.stories.tsx b/web/src/components/SelectionSummaries.stories.tsx new file mode 100644 index 0000000..ca4911a --- /dev/null +++ b/web/src/components/SelectionSummaries.stories.tsx @@ -0,0 +1,143 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import AggregatedBlockSummary from '@components/AggregatedBlockSummary'; +import AggregatedFramesSummary from '@components/AggregatedFramesSummary'; +import BYOFrameBlockSummary from '@components/BYOFrameBlockSummary'; +import FrameBlockSummary from '@components/FrameBlockSummary'; +import FrameSummary from '@components/FrameSummary'; +import { + getProcessedStoryFrame, + storyApiHandlers, + storyBlockRoots, + storyFrameIds, +} from '@app/stories/fixtures'; +import { PanelSurface, withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Components/Selection Summaries', + parameters: { + layout: 'fullscreen', + msw: { + handlers: storyApiHandlers(), + }, + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const FrameLoaded: Story = { + render: () => ( + + + + ), +}; + +export const FrameLoading: Story = { + tags: ['test-exclude'], + parameters: { + msw: { + handlers: storyApiHandlers({ frameDelay: 10_000 }), + }, + }, + render: () => ( + + + + ), +}; + +export const FrameError: Story = { + parameters: { + msw: { + handlers: storyApiHandlers({ frameError: true }), + }, + }, + render: () => ( + + + + ), +}; + +export const FrameBlockLoaded: Story = { + render: () => ( + + + + ), +}; + +export const FrameBlockMissing: Story = { + render: () => ( + + + + ), +}; + +export const AggregatedFramesLoaded: Story = { + render: () => ( + + + + ), +}; + +export const AggregatedFramesPartialError: Story = { + render: () => ( + + + + ), +}; + +export const AggregatedBlockLoaded: Story = { + render: () => ( + + + + ), +}; + +export const AggregatedBlockMissing: Story = { + render: () => ( + + + + ), +}; + +export const BYOFrameBlockLoaded: Story = { + decorators: [ + withForkyProviders({ + focus: { + byo: true, + byoData: getProcessedStoryFrame(storyFrameIds[0]), + }, + }), + ], + render: () => ( + + + + ), +}; + +export const BYOFrameBlockMissingData: Story = { + decorators: [ + withForkyProviders({ + focus: { + byo: true, + }, + }), + ], + render: () => ( + + + + ), +}; diff --git a/web/src/components/Share.tsx b/web/src/components/Share.tsx index c6c5965..ba62e44 100644 --- a/web/src/components/Share.tsx +++ b/web/src/components/Share.tsx @@ -1,13 +1,13 @@ -import { memo, Fragment, useState, FocusEvent, useEffect } from 'react'; +import { memo, useState, FocusEvent } from 'react'; -import { DialogPanel, TransitionChild, Transition, Dialog } from '@headlessui/react'; +import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'; import { ShareIcon, ClipboardDocumentCheckIcon } from '@heroicons/react/24/outline'; -import { useLocation } from 'wouter'; +import { usePathname } from '@hooks/useAppNavigation'; import useFocus from '@contexts/focus'; function Share() { - const [location] = useLocation(); + const location = usePathname(); const { time, playing } = useFocus(); const [open, setOpen] = useState(false); @@ -21,81 +21,68 @@ function Share() { return url.toString(); } - useEffect(() => { + // Close the dialog when the route actually changes, adjusting state + // during render instead of in an effect. + const [prevLocation, setPrevLocation] = useState(location); + if (prevLocation !== location) { + setPrevLocation(location); if (open) { setOpen(false); } - }, [location, open]); + } return ( <> setOpen(true)} title="Share link" - className="fixed z-10 right-6 lg:right-8 top-20 text-stone-700 dark:text-stone-300 cursor-pointer w-10 h-10 rounded-md transition hover:bg-stone-900/5 dark:hover:bg-white/5" + className="glass-chrome fixed top-16 right-4 z-10 flex size-9 cursor-pointer items-center justify-center rounded-lg border border-border text-muted shadow-md transition-colors duration-150 hover:text-foreground lg:right-6" > - + - - - -
- + + -
-
- - -
-
-
- -
-
-
-
- -
-
-
-
+
+
+ +

+ Share this view +

+
+ +
+
+ +
+
-
- +
+
); } diff --git a/web/src/components/Slot.tsx b/web/src/components/Slot.tsx index 40771f8..28aac8c 100644 --- a/web/src/components/Slot.tsx +++ b/web/src/components/Slot.tsx @@ -2,7 +2,7 @@ import React, { useMemo, ReactNode } from 'react'; import classNames from 'clsx'; -import { FrameMetaData } from '@app/types/api'; +import { FrameMetadata } from '@api'; import Ruler from '@components/Ruler'; import SnapshotMarker from '@components/SnapshotMarker'; import useEthereum from '@contexts/ethereum'; @@ -33,7 +33,7 @@ function Slot({ subMarks, slot, shouldFetch = false, segments }: Props) { const groupedMarkers = useMemo(() => { const groupSize = 100 / segments; const groups = - data?.reduce>((acc, frame) => { + data?.reduce>((acc, frame) => { if (node && frame.node !== node) return acc; const timeIntoSlot = new Date(frame.fetched_at).getTime() - slotStart; @@ -55,13 +55,9 @@ function Slot({ subMarks, slot, shouldFetch = false, segments }: Props) { return ( -
-
+
+ {epoch && ( +

- {epoch && ( -

- EPOCH {epoch} -

- )} - {slot && ( -

- SLOT {slot} -

- )} -
-
+ > + EPOCH {epoch} +

+ )} + {slot && ( +

+ SLOT {slot} +

+ )} +
); } diff --git a/web/src/components/SlotDial.tsx b/web/src/components/SlotDial.tsx index 324cb30..1fba1fd 100644 --- a/web/src/components/SlotDial.tsx +++ b/web/src/components/SlotDial.tsx @@ -51,7 +51,7 @@ const SlotDial = () => { }); return ( -
+
{leftSideRulers}
diff --git a/web/src/components/SnapshotMarker.tsx b/web/src/components/SnapshotMarker.tsx index 6652d00..e1cf9e9 100644 --- a/web/src/components/SnapshotMarker.tsx +++ b/web/src/components/SnapshotMarker.tsx @@ -3,7 +3,7 @@ import { memo, useMemo } from 'react'; import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'; import classNames from 'clsx'; -import { FrameMetaData } from '@app/types/api'; +import { FrameMetadata } from '@api'; import useSelection from '@contexts/selection'; function SnapshotMarker({ @@ -11,7 +11,7 @@ function SnapshotMarker({ activeIds, percentage, }: { - metadata: FrameMetaData[]; + metadata: FrameMetadata[]; activeIds: string[]; percentage: string; }) { @@ -25,26 +25,26 @@ function SnapshotMarker({ const isActive = activeIds.includes(metadata[i].id); const isReorg = metadata[i].event_source === 'xatu_reorg_event'; - let color = 'bg-sky-400 dark:bg-sky-700'; + let color = 'bg-marker-time'; if (isReorg) { - color = 'dark:bg-amber-400 bg-amber-700'; + color = 'bg-marker'; if (isActive) { - color = 'bg-amber-600 dark:bg-amber-500'; + color = 'bg-marker-active'; } } else if (isActive) { - color = 'bg-sky-600 dark:bg-sky-500'; + color = 'bg-marker-time-active'; } segments.push(
, ); @@ -84,15 +84,15 @@ function SnapshotMarker({ {segments} - + {metadata.map(meta => (
setFrameId(meta.id)} > -
{meta.id}
-
{meta.node}
+
{meta.id}
+
{meta.node}
))}
diff --git a/web/src/components/Summary.stories.tsx b/web/src/components/Summary.stories.tsx new file mode 100644 index 0000000..e17ff61 --- /dev/null +++ b/web/src/components/Summary.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import { + SummaryCard, + SummaryLink, + SummaryRow, + SummarySection, + ValidityBadge, +} from '@components/Summary'; +import { PanelSurface, withForkyProviders } from '@app/stories/storybook'; +import { storyBlockRoots } from '@app/stories/fixtures'; + +const meta = { + title: 'Components/Summary', + parameters: { + layout: 'fullscreen', + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const CardWithRowsSectionsAndLinks: Story = { + render: () => ( + + + + frame-alpha + + 2024-01-01T00:21:25.000Z + + ams3-teku-001 + + + {storyBlockRoots.slot107} + + + ams3 + v24.12.0 + + + ), +}; + +export const ValidityBadges: Story = { + render: () => ( + +
+ + + + +
+
+ ), +}; diff --git a/web/src/components/Summary.tsx b/web/src/components/Summary.tsx new file mode 100644 index 0000000..67fb33d --- /dev/null +++ b/web/src/components/Summary.tsx @@ -0,0 +1,101 @@ +import { ReactNode } from 'react'; + +import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'; +import classNames from 'clsx'; + +import Link from '@components/Link'; + +/** + * Shared building blocks for the slide-out selection panel: a compact, + * consistent key/value card used by every summary state (snapshot, block, + * aggregated, BYO). + */ + +export function SummaryCard({ children }: { children: ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +export function SummaryRow({ + label, + mono = false, + children, +}: { + label: string; + mono?: boolean; + children: ReactNode; +}) { + return ( +
+
+ {label} +
+
+ {children} +
+
+ ); +} + +export function SummarySection({ title }: { title: string }) { + return ( +
+
+ {title} +
+
+ ); +} + +export function SummaryLink({ + href, + onClick, + children, +}: { + href: string; + onClick?: () => void; + children: ReactNode; +}) { + return ( + + {children} +
-
-

+

+

{slots.toLocaleString()}

-

+

slots hidden

diff --git a/web/src/components/Walker.tsx b/web/src/components/Walker.tsx index 8e9775b..c86eab8 100644 --- a/web/src/components/Walker.tsx +++ b/web/src/components/Walker.tsx @@ -34,7 +34,10 @@ const Walker: React.FC = ({ const stepSize = 10; const walkerCount = 5; const angles = [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]; - const colors = ['#241e44', '#25315e', '#3a5c85', '#56a1bf', '#97dbd2']; + const rootStyles = getComputedStyle(document.documentElement); + const colors = [1, 2, 3, 4, 5].map(i => + rootStyles.getPropertyValue(`--color-walker-${i}`).trim(), + ); if (!walkerContext) return; diff --git a/web/src/components/WeightedNode.tsx b/web/src/components/WeightedNode.tsx index c63344d..f84f4f1 100644 --- a/web/src/components/WeightedNode.tsx +++ b/web/src/components/WeightedNode.tsx @@ -1,9 +1,59 @@ -import { useState, memo } from 'react'; +import { memo } from 'react'; import classNames from 'clsx'; import ProgressCircle from '@components/ProgressCircle'; +/* Status visual recipe: dim status-tinted core, bright status rim (the + * progress arc) and a soft glow so state reads at any zoom level. */ +const NODE_STYLES: Record< + string, + { fill: string; arc: string; track: string; label: string; glow: string } +> = { + canonical: { + fill: 'bg-canonical-deep', + arc: 'text-canonical-ring', + track: 'text-canonical-edge', + label: 'text-canonical', + glow: 'shadow-[0_0_90px_-15px_var(--color-canonical-ring)]', + }, + fork: { + fill: 'bg-fork-deep', + arc: 'text-fork-ring', + track: 'text-fork-edge', + label: 'text-fork', + glow: 'shadow-[0_0_90px_-15px_var(--color-fork-ring)]', + }, + finalized: { + fill: 'bg-finalized-deep', + arc: 'text-finalized-ring', + track: 'text-finalized-edge', + label: 'text-finalized', + glow: 'shadow-[0_0_90px_-15px_var(--color-finalized-ring)]', + }, + justified: { + fill: 'bg-justified-deep', + arc: 'text-justified-ring', + track: 'text-justified-edge', + label: 'text-justified', + glow: 'shadow-[0_0_90px_-15px_var(--color-justified-ring)]', + }, + invalid: { + fill: 'bg-invalid-deep', + arc: 'text-invalid-ring', + track: 'text-invalid-edge', + label: 'text-invalid', + glow: 'shadow-[0_0_90px_-15px_var(--color-invalid-ring)]', + }, + optimistic: { + fill: 'bg-optimistic-deep', + arc: 'text-optimistic-ring', + track: 'text-optimistic-edge', + label: 'text-optimistic', + glow: 'shadow-[0_0_90px_-15px_var(--color-optimistic-ring)]', + }, +}; + function WeightedNode({ id, hash, @@ -29,50 +79,26 @@ function WeightedNode({ className?: string; onClick?: (hash: string) => void; }) { - const [isHighlighted, setIsHighlighted] = useState(false); - - const [color, backgroundColor, borderColor] = (() => { - if (!['valid', 'optimistic'].includes(validity.toLowerCase())) { - return ['text-red-600', 'text-red-800', 'border-red-500 dark:border-red-900']; - } - switch (type) { - case 'canonical': - if (validity.toLowerCase() === 'optimistic') { - return ['text-yellow-600', 'text-yellow-800', 'border-yellow-500 dark:border-yellow-900']; - } - return [ - 'text-emerald-600', - 'text-emerald-800', - 'border-emerald-500 dark:border-emerald-900', - ]; - case 'fork': - return ['text-amber-600', 'text-amber-800', 'border-amber-500 dark:border-amber-900']; - case 'finalized': - return [ - 'text-fuchsia-600', - 'text-fuchsia-800', - 'border-fuchsia-500 dark:border-fuchsia-900', - ]; - case 'justified': - return ['text-indigo-600', 'text-indigo-800', 'border-indigo-500 dark:border-indigo-900']; - case 'detached': - return ['text-red-600', 'text-red-800', 'border-red-500 dark:border-red-900']; - default: - return [ - 'text-emerald-600', - 'text-emerald-800', - 'border-emerald-500 dark:border-emerald-900', - ]; - } + const styleKey = (() => { + if (!['valid', 'optimistic'].includes(validity.toLowerCase())) return 'invalid'; + if (type === 'detached') return 'invalid'; + if (type === 'canonical' && validity.toLowerCase() === 'optimistic') return 'optimistic'; + return type; })(); + const styles = NODE_STYLES[styleKey] ?? NODE_STYLES.canonical; + + const label = + type === 'finalized' || type === 'justified' || type === 'detached' + ? type.toUpperCase() + : validity.toUpperCase(); return (
onClick?.(hash)} - onMouseEnter={() => setIsHighlighted(true)} - onMouseLeave={() => setIsHighlighted(false)} > -

- {type === 'finalized' || type === 'justified' || type === 'detached' - ? type.toUpperCase() - : validity.toUpperCase()} -

-

- {hash.substring(0, 6)}...{hash.substring(hash.length - 4)} -

- {weight && weight !== '0' ? weight : '\u00a0'} + {label} +

+

+ {hash.substring(0, 6)}…{hash.substring(hash.length - 4)} +

+

+ {weight && weight !== '0' ? weight : ' '}

); diff --git a/web/src/contexts/focus.tsx b/web/src/contexts/focus.tsx index 6bb2a2d..b2d5516 100644 --- a/web/src/contexts/focus.tsx +++ b/web/src/contexts/focus.tsx @@ -56,22 +56,26 @@ export function useValue(props: ValueProps): State { const [byoData, setBYODataInternal] = useState(); const { secondsPerSlot, slotsPerEpoch, genesisTime } = useEthereum(); const timer = useRef(null); - const initialPlayTime = useRef( - props.playing ? new Date().getTime() : undefined, - ); + const initialPlayTime = useRef(undefined); const [playing, setPlaying] = useState(props.playing); - useEffect(() => { + // Sync route-driven props into state during render (React's documented + // pattern for adjusting state when props change). + const [prevRouteProps, setPrevRouteProps] = useState({ + node: props.node, + byo: props.byo, + frameId: props.frameId, + }); + if ( + prevRouteProps.node !== props.node || + prevRouteProps.byo !== props.byo || + prevRouteProps.frameId !== props.frameId + ) { + setPrevRouteProps({ node: props.node, byo: props.byo, frameId: props.frameId }); if (props.node !== node) setNode(props.node); - }, [props.node, node]); - - useEffect(() => { if (props.byo !== byo) setBYO(props.byo); - }, [props.byo, byo]); - - useEffect(() => { if (props.frameId !== frameId) setFrameId(props.frameId); - }, [props.frameId, frameId]); + } const setTimeWrapper = useCallback( (update: number) => { @@ -97,34 +101,32 @@ export function useValue(props: ValueProps): State { [setTime, genesisTime], ); + const stepRef = useRef<() => void>(() => {}); + const step = useCallback(() => { if (!initialPlayTime.current) initialPlayTime.current = new Date().getTime(); const currentTime = new Date().getTime(); const timeDiff = currentTime - (initialPlayTime.current ?? 0); shiftTime(timeDiff); initialPlayTime.current = currentTime; - timer.current = requestAnimationFrame(step); + timer.current = requestAnimationFrame(() => stepRef.current()); }, [shiftTime]); + useEffect(() => { + stepRef.current = step; + }); + const play = useCallback( (offset?: number) => { - if (timer.current) cancelAnimationFrame(timer.current); - initialPlayTime.current = new Date().getTime(); if (offset) shiftTime(offset); - timer.current = requestAnimationFrame(step); setPlaying(true); }, - [shiftTime, step], + [shiftTime], ); const stop = useCallback(() => { - initialPlayTime.current = undefined; - if (timer.current) { - cancelAnimationFrame(timer.current); - timer.current = null; - } setPlaying(false); - }, [setPlaying]); + }, []); const setSlot = useCallback( (targetSlot: number) => { @@ -155,12 +157,23 @@ export function useValue(props: ValueProps): State { [setBYODataInternal], ); + // The animation-frame loop is an external system: subscribe to it while + // playing and tear it down on stop/unmount. useEffect(() => { - if (playing) play(); + if (!playing) return; + + if (timer.current) cancelAnimationFrame(timer.current); + initialPlayTime.current = new Date().getTime(); + timer.current = requestAnimationFrame(() => stepRef.current()); + return () => { - stop(); + initialPlayTime.current = undefined; + if (timer.current) { + cancelAnimationFrame(timer.current); + timer.current = null; + } }; - }, [playing, play, stop]); + }, [playing]); useEffect(() => { return () => { diff --git a/web/src/contexts/selection.tsx b/web/src/contexts/selection.tsx index b3f219d..e6e0e41 100644 --- a/web/src/contexts/selection.tsx +++ b/web/src/contexts/selection.tsx @@ -1,6 +1,6 @@ import { useContext as reactUseContext, createContext, useState, useCallback } from 'react'; -import { ForkChoiceNode, FrameMetaData } from '@app/types/api'; +import { ForkChoiceNode, FrameMetadata } from '@api'; import { WeightedNodeAttributes, AggregatedNodeAttributes } from '@app/types/graph'; export const Context = createContext(undefined); @@ -14,7 +14,7 @@ export default function useContext() { } export type WeightedNode = { - metadata: FrameMetaData; + metadata: FrameMetadata; attributes: WeightedNodeAttributes; node: ForkChoiceNode; }; @@ -23,7 +23,7 @@ export type AggregatedNode = { nodes: Record< string, { - metadata: FrameMetaData; + metadata: FrameMetadata; attributes: WeightedNodeAttributes; node: ForkChoiceNode; } diff --git a/web/src/hooks/__tests__/useActive.test.ts b/web/src/hooks/__tests__/useActive.test.ts index d8fddb6..dfe5783 100644 --- a/web/src/hooks/__tests__/useActive.test.ts +++ b/web/src/hooks/__tests__/useActive.test.ts @@ -1,14 +1,14 @@ import { renderHook } from '@testing-library/react'; import { vi, Mock } from 'vitest'; -import { FrameMetaData } from '@app/types/api'; +import { FrameMetadata } from '@api'; import useActiveFrame, { findLatestFrameIdPerNode } from '@hooks/useActive'; import { useMetadataQuery } from '@hooks/useQuery'; import { ProviderWrapper } from '@utils/testing'; vi.mock('@hooks/useQuery'); -const metadataMock: FrameMetaData[] = [ +const metadataMock: FrameMetadata[] = [ { id: '1', node: 'node1', @@ -168,7 +168,7 @@ describe('useActive', () => { }); it('should ignore reorg frames when finding the latest frame ids per node', () => { - const reorgMetadata: FrameMetaData = { + const reorgMetadata: FrameMetadata = { id: '55', node: 'node1', fetched_at: '2000-01-01T02:10:00.000Z', @@ -223,7 +223,7 @@ describe('useActive', () => { }); it('should return the latest frame ids per node considering metadataMinus2', () => { - const metadataMinus2Mock: FrameMetaData = { + const metadataMinus2Mock: FrameMetadata = { id: '3', node: 'node1', fetched_at: '2000-01-01T00:40:00.000Z', diff --git a/web/src/hooks/__tests__/useGraph.test.ts b/web/src/hooks/__tests__/useGraph.test.ts index 3366300..9c9541b 100644 --- a/web/src/hooks/__tests__/useGraph.test.ts +++ b/web/src/hooks/__tests__/useGraph.test.ts @@ -144,22 +144,24 @@ describe('useGraph', () => { }, ); - const x = result.current.nodes[result.current.nodes.length - 1].x; - const y = result.current.nodes[result.current.nodes.length - 1].y; + // Compare full position arrays: single-node comparisons are flaky because + // a node on the canonical row keeps y=0 at any spacing. + const positions = result.current.nodes.map(({ x, y }) => ({ x, y })); + const serialize = () => JSON.stringify(result.current.nodes.map(({ x, y }) => ({ x, y }))); rerender({ data, spacingX: 15, spacingY: 15 }); - expect(x).not.toEqual(result.current.nodes[result.current.nodes.length - 1].x); - expect(y).not.toEqual(result.current.nodes[result.current.nodes.length - 1].y); + expect(serialize()).not.toEqual(JSON.stringify(positions)); rerender({ data, spacingX: 10, spacingY: 15 }); - expect(x).toEqual(result.current.nodes[result.current.nodes.length - 1].x); - expect(y).not.toEqual(result.current.nodes[result.current.nodes.length - 1].y); + result.current.nodes.forEach((node, i) => { + expect(node.x).toEqual(positions[i].x); + }); + expect(serialize()).not.toEqual(JSON.stringify(positions)); rerender({ data, spacingX: 10, spacingY: 10 }); - expect(x).toEqual(result.current.nodes[result.current.nodes.length - 1].x); - expect(y).toEqual(result.current.nodes[result.current.nodes.length - 1].y); + expect(serialize()).toEqual(JSON.stringify(positions)); }); }); diff --git a/web/src/hooks/useActive.ts b/web/src/hooks/useActive.ts index a30d1d4..8c54af6 100644 --- a/web/src/hooks/useActive.ts +++ b/web/src/hooks/useActive.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { FrameMetaData } from '@app/types/api'; +import { FrameMetadata } from '@api'; import useFocus from '@contexts/focus'; import { useMetadataQuery } from '@hooks/useQuery'; @@ -11,7 +11,7 @@ interface State { export function findLatestFrameIdPerNode( focusedTime: number, - metadata: FrameMetaData[] = [], + metadata: FrameMetadata[] = [], focusedNode?: string, ): State { // Filter metadata @@ -23,7 +23,7 @@ export function findLatestFrameIdPerNode( }); // Group metadata by node - let groupedMetadata: { [key: string]: FrameMetaData[] } = {}; + let groupedMetadata: { [key: string]: FrameMetadata[] } = {}; for (const frame of metadata) { if (!groupedMetadata[frame.node]) { groupedMetadata[frame.node] = []; diff --git a/web/src/hooks/useAppNavigation.ts b/web/src/hooks/useAppNavigation.ts new file mode 100644 index 0000000..c50102e --- /dev/null +++ b/web/src/hooks/useAppNavigation.ts @@ -0,0 +1,74 @@ +import { useCallback } from 'react'; + +import { useLocation, useNavigate } from '@tanstack/react-router'; + +import { parseAppRoute } from '@utils/routes'; + +export type NavigateFn = (to: string, options?: { replace?: boolean }) => void; + +/** + * Forky's components pass plain app path strings around. These hooks are the + * boundary that turns those paths into TanStack Router's typed route targets. + */ + +/** The current pathname. */ +export function usePathname(): string { + return useLocation({ select: location => location.pathname }); +} + +/** Navigate to an app path string via the matching typed route. */ +export function useAppNavigate(): NavigateFn { + const routerNavigate = useNavigate(); + + return useCallback( + (to, options) => { + const route = parseAppRoute(to); + + switch (route?.kind) { + case 'home': + routerNavigate({ to: '/', replace: options?.replace }); + return; + case 'events': + routerNavigate({ to: '/events', replace: options?.replace }); + return; + case 'byo': + routerNavigate({ to: '/byo', replace: options?.replace }); + return; + case 'byoEvents': + routerNavigate({ to: '/byo/events', replace: options?.replace }); + return; + case 'node': + routerNavigate({ + to: '/node/$', + params: { _splat: route.nodeId }, + replace: options?.replace, + }); + return; + case 'nodeEvents': + routerNavigate({ + to: '/node/$', + params: { _splat: `${route.nodeId}/events` }, + replace: options?.replace, + }); + return; + case 'snapshot': + routerNavigate({ + to: '/snapshot/$frameId', + params: { frameId: route.frameId }, + replace: options?.replace, + }); + return; + case 'snapshotEvents': + routerNavigate({ + to: '/snapshot/$frameId/events', + params: { frameId: route.frameId }, + replace: options?.replace, + }); + return; + default: + window.location.assign(to); + } + }, + [routerNavigate], + ); +} diff --git a/web/src/hooks/useNow.ts b/web/src/hooks/useNow.ts new file mode 100644 index 0000000..221fff4 --- /dev/null +++ b/web/src/hooks/useNow.ts @@ -0,0 +1,23 @@ +import { useCallback, useSyncExternalStore } from 'react'; + +/** + * Returns the current time in epoch milliseconds, updating every + * `intervalMs`. The clock is an external system, so it is exposed through + * useSyncExternalStore to keep renders pure. + */ +export default function useNow(intervalMs = 1000): number { + const subscribe = useCallback( + (onStoreChange: () => void) => { + const id = window.setInterval(onStoreChange, intervalMs); + return () => window.clearInterval(id); + }, + [intervalMs], + ); + + const getSnapshot = useCallback( + () => Math.floor(Date.now() / intervalMs) * intervalMs, + [intervalMs], + ); + + return useSyncExternalStore(subscribe, getSnapshot); +} diff --git a/web/src/hooks/useQuery.ts b/web/src/hooks/useQuery.ts index 7c7aae8..b6c7020 100644 --- a/web/src/hooks/useQuery.ts +++ b/web/src/hooks/useQuery.ts @@ -1,56 +1,95 @@ -import { useQuery, useQueries } from '@tanstack/react-query'; +import { useQueries, useQuery } from '@tanstack/react-query'; -import { fetchNow, fetchSpec, Spec } from '@api/ethereum'; -import { fetchFrame } from '@api/frames'; -import { fetchMetadataList, fetchMetadataNodes } from '@api/metadata'; -import { FrameFilter, FrameMetaData, V1GetEthereumNowResponse } from '@app/types/api'; +import { getEthereumNow, getEthereumSpec, getFrame, listMetadata, listMetadataNodes } from '@api'; +import { EthereumSpec, Frame, FrameMetadata } from '@api'; +import { FrameFilter } from '@app/types/api'; import { ProcessedData } from '@app/types/graph'; +import { processForkChoiceData } from '@utils/graph'; + +export interface Spec { + network_name: string; + spec: EthereumSpec; +} export function useNowQuery(enabled = true) { - return useQuery< - Required, - unknown, - Required, - string[] - >({ + return useQuery({ queryKey: ['now'], - queryFn: () => fetchNow(), + queryFn: async () => { + const { data, error } = await getEthereumNow(); + const now = data?.data; + if (error || now?.slot == null || now?.epoch == null) { + throw new Error('Failed to fetch ethereum now'); + } + return { slot: now.slot, epoch: now.epoch }; + }, enabled, staleTime: 60_000, }); } export function useSpecQuery(enabled = true) { - return useQuery({ + return useQuery({ queryKey: ['spec'], - queryFn: () => fetchSpec(), + queryFn: async () => { + const { data, error } = await getEthereumSpec(); + const result = data?.data; + if (error || !result?.spec) { + throw new Error('Failed to fetch ethereum spec'); + } + return { + network_name: result.network_name ?? 'unknown', + spec: result.spec as EthereumSpec, + }; + }, enabled, staleTime: 60_000, }); } export function useNodesQuery(filter: FrameFilter, enabled = true) { - return useQuery({ - queryKey: ['metadata', filter], - queryFn: () => fetchMetadataNodes({ pagination: { limit: 100 }, filter }), + return useQuery({ + queryKey: ['metadata-nodes', filter], + queryFn: async () => { + const { data, error } = await listMetadataNodes({ + body: { pagination: { limit: 100 }, filter }, + }); + if (error) throw new Error('Failed to fetch metadata nodes'); + return data?.data?.nodes ?? []; + }, enabled, staleTime: 6_000, }); } export function useMetadataQuery(filter: FrameFilter, enabled = true) { - return useQuery({ + return useQuery({ queryKey: ['metadata', filter], - queryFn: () => fetchMetadataList({ pagination: { limit: 1000 }, filter }), + queryFn: async () => { + const { data, error } = await listMetadata({ + body: { pagination: { limit: 1000 }, filter }, + }); + if (error) throw new Error('Failed to fetch metadata list'); + return (data?.data?.frames ?? []) satisfies FrameMetadata[]; + }, enabled, staleTime: 6_000, }); } +async function fetchProcessedFrame(id: string): Promise { + const { data, error } = await getFrame({ path: { id } }); + const frame = data?.data?.frame; + if (error || !frame?.data || !frame?.metadata) { + throw new Error('Failed to fetch frame'); + } + + return processForkChoiceData(frame satisfies Required); +} + export function useFrameQuery(id: string, enabled = true) { - return useQuery({ + return useQuery({ queryKey: ['frame', id], - queryFn: () => fetchFrame(id), + queryFn: () => fetchProcessedFrame(id), enabled, staleTime: 120_000, }); @@ -60,7 +99,7 @@ export function useFrameQueries(ids: string[], enabled = true) { return useQueries({ queries: ids.map(id => ({ queryKey: ['frame', id], - queryFn: () => fetchFrame(id), + queryFn: () => fetchProcessedFrame(id), enabled, staleTime: 120_000, })), diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..936c944 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,444 @@ +/** + * Main CSS Entry Point + * + * "Chain Console" design language: a dark-first, cool-neutral canvas where + * the six fork-choice status colors are the only loud thing on screen, + * framed by quiet glass chrome. Light mode is a faithful inversion. + * + * Two-Tier Token Architecture + * + * Tier 1: Primitive color values (plain CSS variables, not exposed as + * Tailwind utilities): the "ink" cool-neutral ramp (hue 270), the + * cyan accent family and the six status families. Status bands + * share OKLCH lightness/chroma per role so every status holds the + * same contrast against the surface ladder in both themes. + * Tier 2: Semantic tokens (@theme), named for intent. These are the ONLY + * colors application code may use (enforced by the custom ESLint + * rules in eslint-rules/). Light/dark theming happens here — app + * code never writes `dark:` variants for colors. + */ + +@import '@fontsource-variable/inter'; +@import '@fontsource-variable/jetbrains-mono'; +@import 'tailwindcss'; + +/* Enable class-based dark mode */ +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + /* ===== Typography ===== */ + --font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif; + --font-display: 'Inter Variable', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'JetBrains Mono Variable', ui-monospace, 'SF Mono', Menlo, monospace; + + /* ===== TIER 2: Semantic tokens ===== */ + + /* Surfaces (elevation = lighter surface in dark mode) */ + --color-background: var(--background-value); + --color-surface: var(--surface-value); + --color-surface-raised: var(--surface-raised-value); + --color-overlay: var(--overlay-value); + --color-shell: var(--shell-value); + + /* Borders and separators (alpha hairlines: they read identically over + * glass, gradients and surfaces) */ + --color-border: var(--border-value); + --color-border-strong: var(--border-strong-value); + + /* Text */ + --color-foreground: var(--foreground-value); + --color-foreground-strong: var(--foreground-strong-value); + --color-foreground-inverted: var(--foreground-inverted-value); + --color-muted: var(--muted-value); + --color-faint: var(--faint-value); + + /* Accent (electric cyan): focus rings, links, live playhead, brand. + * Sits in the largest hue gap between the status hues so it never + * collides with node semantics. */ + --color-accent: var(--accent-value); + --color-accent-hover: var(--accent-hover-value); + + /* Controls (sliders, toggles, scrubbers, interactive emphasis) */ + --color-track: var(--track-value); + --color-active: var(--active-value); + --color-active-hover: var(--active-hover-value); + --color-surface-hover: var(--surface-hover-value); + --color-field: var(--field-value); + --color-control: var(--control-value); + --color-control-hover: var(--control-hover-value); + --color-toolbar: var(--toolbar-value); + --color-timeline: var(--timeline-value); + --color-timeline-edge: var(--timeline-edge-value); + --color-timeline-fg: var(--timeline-fg-value); + --color-dial: var(--dial-value); + + /* Primary action (share/copy buttons) */ + --color-primary: var(--primary-value); + --color-primary-hover: var(--primary-hover-value); + --color-on-primary: var(--on-primary-value); + + /* Graph rendering */ + --color-edge: var(--edge-value); + --color-live: var(--live-value); + + /* Brand and links */ + --color-brand: var(--brand-value); + --color-link: var(--link-value); + --color-link-hover: var(--link-hover-value); + + /* Modal/drawer backdrop */ + --color-scrim: var(--scrim-value); + + /* State */ + --color-success: var(--success-value); + --color-warning: var(--warning-value); + --color-caution: var(--caution-value); + --color-danger: var(--danger-value); + --color-danger-surface: var(--danger-surface-value); + --color-danger-accent: var(--danger-accent-value); + + /* Fork-choice domain: block/node states in the graph visualization. + * Roles: base = status text (AA on surfaces), `-deep` = node core fill + * (dim in dark, tint in light), `-edge` = rim track under the progress + * arc, `-ring` = the bright progress-arc / rim shade. */ + --color-canonical: var(--canonical-value); + --color-canonical-deep: var(--canonical-deep-value); + --color-canonical-edge: var(--canonical-edge-value); + --color-canonical-ring: var(--canonical-ring-value); + --color-fork: var(--fork-value); + --color-fork-deep: var(--fork-deep-value); + --color-fork-edge: var(--fork-edge-value); + --color-fork-ring: var(--fork-ring-value); + --color-finalized: var(--finalized-value); + --color-finalized-deep: var(--finalized-deep-value); + --color-finalized-edge: var(--finalized-edge-value); + --color-finalized-ring: var(--finalized-ring-value); + --color-justified: var(--justified-value); + --color-justified-deep: var(--justified-deep-value); + --color-justified-edge: var(--justified-edge-value); + --color-justified-ring: var(--justified-ring-value); + --color-invalid: var(--invalid-value); + --color-invalid-deep: var(--invalid-deep-value); + --color-invalid-edge: var(--invalid-edge-value); + --color-invalid-ring: var(--invalid-ring-value); + --color-optimistic: var(--optimistic-value); + --color-optimistic-deep: var(--optimistic-deep-value); + --color-optimistic-edge: var(--optimistic-edge-value); + --color-optimistic-ring: var(--optimistic-ring-value); + + /* Timeline markers */ + --color-marker: var(--marker-value); + --color-marker-active: var(--marker-active-value); + --color-marker-time: var(--marker-time-value); + --color-marker-time-active: var(--marker-time-active-value); + + /* Playback scrubber */ + --color-scrubber: var(--scrubber-value); + --color-scrubber-strong: var(--scrubber-strong-value); + --color-rec: var(--rec-value); + --color-rec-strong: var(--rec-strong-value); + + /* Ruler ticks */ + --color-tick: var(--tick-value); + --color-tick-label: var(--tick-label-value); + + /* Text on the always-dark shell */ + --color-on-shell: var(--ink-100); +} + +/* Landing hero / wordmark gradient (canonical emerald → accent cyan). */ +.hero-gradient-text { + background-image: linear-gradient( + to right, + oklch(78% 0.155 163), + oklch(80% 0.13 195) + ); + background-clip: text; + color: transparent; +} + +html:not(.dark) .hero-gradient-text { + background-image: linear-gradient(to right, oklch(46% 0.11 163), oklch(52% 0.12 205)); +} + +/* Vertical fade behind slot columns in the graph (transparent → hairline → + * transparent). Kept in CSS so the gradient stops stay in the theme. */ +.column-fade { + background-image: linear-gradient( + to top, + transparent 5%, + var(--column-fade-mid) 50%, + transparent 95% + ); +} + +/* Infinite-canvas dot grid for the graph stage. */ +.stage-grid { + background-image: radial-gradient(var(--grid-dot-color) 1.5px, transparent 1.5px); + background-size: 28px 28px; +} + +/* Glass chrome: the floating console dock + blurred header. Glass is for + * chrome only — content panels stay solid for readability. */ +.glass-chrome { + background-color: color-mix(in oklab, var(--toolbar-value) 82%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + box-shadow: inset 0 1px 0 var(--glass-highlight); +} + +@layer base { + body { + background-color: var(--background-value); + color: var(--foreground-value); + } + + :root { + /* ===== TIER 1: Primitive values ===== */ + + /* Ink: cool near-neutral ramp (hue 270) */ + --ink-0: oklch(98.5% 0.002 270); + --ink-50: oklch(95.5% 0.004 270); + --ink-100: oklch(92% 0.005 270); + --ink-200: oklch(87% 0.006 270); + --ink-300: oklch(78% 0.008 270); + --ink-400: oklch(66% 0.01 270); + --ink-500: oklch(55% 0.01 270); + --ink-600: oklch(44% 0.011 270); + --ink-700: oklch(33% 0.011 270); + --ink-800: oklch(26.5% 0.011 270); + --ink-850: oklch(22.5% 0.01 270); + --ink-900: oklch(18.5% 0.009 270); + --ink-950: oklch(14.5% 0.008 270); + + /* Accent: electric cyan */ + --accent-bright: oklch(78% 0.13 200); + --accent-brighter: oklch(85% 0.12 198); + --accent-deep: oklch(55% 0.12 205); + --accent-deeper: oklch(48% 0.11 207); + + /* Status families. Hues: canonical 163, fork 60, finalized 322, + * justified 277, invalid 25, optimistic 100. Bands (shared L/C per + * role): bright = dark-mode text/arc, dim = dark node fill, shade = + * dark rim track, strong = light-mode text, vivid = light arc, + * tint = light node fill, wash = light rim track. */ + --status-canonical-bright: oklch(72% 0.155 163); + --status-canonical-dim: oklch(40% 0.09 163); + --status-canonical-shade: oklch(30% 0.055 163); + --status-canonical-strong: oklch(46% 0.11 163); + --status-canonical-vivid: oklch(58% 0.14 163); + --status-canonical-tint: oklch(93% 0.06 163); + --status-canonical-wash: oklch(86% 0.07 163); + --status-fork-bright: oklch(72% 0.155 60); + --status-fork-dim: oklch(40% 0.09 60); + --status-fork-shade: oklch(30% 0.055 60); + --status-fork-strong: oklch(46% 0.11 60); + --status-fork-vivid: oklch(58% 0.14 60); + --status-fork-tint: oklch(93% 0.06 60); + --status-fork-wash: oklch(86% 0.07 60); + --status-finalized-bright: oklch(72% 0.155 322); + --status-finalized-dim: oklch(40% 0.09 322); + --status-finalized-shade: oklch(30% 0.055 322); + --status-finalized-strong: oklch(46% 0.11 322); + --status-finalized-vivid: oklch(58% 0.14 322); + --status-finalized-tint: oklch(93% 0.06 322); + --status-finalized-wash: oklch(86% 0.07 322); + --status-justified-bright: oklch(72% 0.155 277); + --status-justified-dim: oklch(40% 0.09 277); + --status-justified-shade: oklch(30% 0.055 277); + --status-justified-strong: oklch(46% 0.11 277); + --status-justified-vivid: oklch(58% 0.14 277); + --status-justified-tint: oklch(93% 0.06 277); + --status-justified-wash: oklch(86% 0.07 277); + --status-invalid-bright: oklch(72% 0.155 25); + --status-invalid-dim: oklch(40% 0.09 25); + --status-invalid-shade: oklch(30% 0.055 25); + --status-invalid-strong: oklch(46% 0.11 25); + --status-invalid-vivid: oklch(58% 0.14 25); + --status-invalid-tint: oklch(93% 0.06 25); + --status-invalid-wash: oklch(86% 0.07 25); + --status-optimistic-bright: oklch(76% 0.155 100); + --status-optimistic-dim: oklch(40% 0.09 100); + --status-optimistic-shade: oklch(30% 0.055 100); + --status-optimistic-strong: oklch(46% 0.11 100); + --status-optimistic-vivid: oklch(58% 0.14 100); + --status-optimistic-tint: oklch(93% 0.06 100); + --status-optimistic-wash: oklch(86% 0.07 100); + + /* Rose (REC / replay indicator) */ + --rose-bright: oklch(71% 0.19 13); + --rose-mid: oklch(64.5% 0.246 16); + --rose-deep: oklch(59% 0.25 17); + + /* Walker loading animation ramp (decorative; read from canvas code via + * getComputedStyle). Ink → accent cyan. */ + --color-walker-1: oklch(25% 0.04 270); + --color-walker-2: oklch(35% 0.07 240); + --color-walker-3: oklch(50% 0.1 220); + --color-walker-4: oklch(65% 0.12 205); + --color-walker-5: oklch(80% 0.13 195); + + /* ===== Semantic mappings: light mode ===== */ + --background-value: var(--ink-0); + --surface-value: #fff; + --surface-raised-value: var(--ink-50); + --overlay-value: var(--ink-950); + --shell-value: var(--ink-950); + --border-value: oklch(0% 0 0 / 8%); + --border-strong-value: oklch(0% 0 0 / 15%); + --foreground-value: oklch(21% 0.01 270); + --foreground-strong-value: var(--ink-950); + --foreground-inverted-value: var(--ink-0); + --muted-value: var(--ink-500); + --faint-value: var(--ink-400); + --accent-value: var(--accent-deep); + --accent-hover-value: var(--accent-deeper); + --track-value: oklch(0% 0 0 / 5%); + --active-value: var(--accent-deep); + --active-hover-value: var(--accent-deeper); + --surface-hover-value: #fff; + --field-value: oklch(0% 0 0 / 4%); + --control-value: var(--accent-deep); + --control-hover-value: var(--accent-deeper); + --toolbar-value: #fff; + --timeline-value: var(--ink-0); + --timeline-edge-value: oklch(0% 0 0 / 8%); + --timeline-fg-value: oklch(21% 0.01 270); + --dial-value: var(--ink-50); + --primary-value: var(--accent-deep); + --primary-hover-value: var(--accent-deeper); + --on-primary-value: #fff; + --edge-value: oklch(0% 0 0 / 18%); + --live-value: var(--accent-deep); + --brand-value: var(--accent-deep); + --link-value: var(--accent-deep); + --link-hover-value: var(--accent-deeper); + --scrim-value: oklch(14.5% 0.008 270 / 40%); + --success-value: var(--status-canonical-strong); + --warning-value: var(--status-fork-strong); + --caution-value: var(--status-optimistic-strong); + --danger-value: oklch(52% 0.19 25); + --danger-surface-value: var(--status-invalid-tint); + /* danger-accent renders on the always-dark shell (error pages) */ + --danger-accent-value: oklch(70% 0.19 20); + --canonical-value: var(--status-canonical-strong); + --canonical-deep-value: var(--status-canonical-tint); + --canonical-edge-value: var(--status-canonical-wash); + --canonical-ring-value: var(--status-canonical-vivid); + --fork-value: var(--status-fork-strong); + --fork-deep-value: var(--status-fork-tint); + --fork-edge-value: var(--status-fork-wash); + --fork-ring-value: var(--status-fork-vivid); + --finalized-value: var(--status-finalized-strong); + --finalized-deep-value: var(--status-finalized-tint); + --finalized-edge-value: var(--status-finalized-wash); + --finalized-ring-value: var(--status-finalized-vivid); + --justified-value: var(--status-justified-strong); + --justified-deep-value: var(--status-justified-tint); + --justified-edge-value: var(--status-justified-wash); + --justified-ring-value: var(--status-justified-vivid); + --invalid-value: var(--status-invalid-strong); + --invalid-deep-value: var(--status-invalid-tint); + --invalid-edge-value: var(--status-invalid-wash); + --invalid-ring-value: var(--status-invalid-vivid); + --optimistic-value: var(--status-optimistic-strong); + --optimistic-deep-value: var(--status-optimistic-tint); + --optimistic-edge-value: var(--status-optimistic-wash); + --optimistic-ring-value: var(--status-optimistic-vivid); + --marker-value: var(--status-fork-vivid); + --marker-active-value: var(--status-fork-strong); + --marker-time-value: oklch(65% 0.08 235); + --marker-time-active-value: var(--accent-deep); + --scrubber-value: var(--ink-500); + --scrubber-strong-value: var(--ink-600); + --rec-value: var(--rose-bright); + --rec-strong-value: var(--rose-mid); + --tick-value: oklch(0% 0 0 / 30%); + --tick-label-value: var(--ink-500); + --column-fade-mid: oklch(0% 0 0 / 12%); + --grid-dot-color: oklch(0% 0 0 / 7%); + --glass-highlight: oklch(100% 0 0 / 60%); + } + + html.dark { + /* ===== Semantic mappings: dark mode ===== */ + --background-value: var(--ink-950); + --surface-value: var(--ink-900); + --surface-raised-value: var(--ink-850); + --overlay-value: #fff; + --shell-value: var(--ink-950); + --border-value: oklch(100% 0 0 / 8%); + --border-strong-value: oklch(100% 0 0 / 15%); + --foreground-value: var(--ink-100); + --foreground-strong-value: var(--ink-0); + --foreground-inverted-value: var(--ink-950); + --muted-value: oklch(72% 0.01 270); + --faint-value: var(--ink-500); + --accent-value: var(--accent-bright); + --accent-hover-value: var(--accent-brighter); + --track-value: oklch(100% 0 0 / 6%); + --active-value: var(--accent-bright); + --active-hover-value: var(--accent-brighter); + --surface-hover-value: var(--ink-800); + --field-value: oklch(100% 0 0 / 5%); + --control-value: var(--accent-bright); + --control-hover-value: var(--accent-brighter); + --toolbar-value: var(--ink-900); + --timeline-value: var(--ink-950); + --timeline-edge-value: oklch(100% 0 0 / 8%); + --timeline-fg-value: var(--ink-100); + --dial-value: var(--ink-950); + --primary-value: var(--accent-bright); + --primary-hover-value: var(--accent-brighter); + --on-primary-value: var(--ink-950); + --edge-value: oklch(100% 0 0 / 20%); + --live-value: var(--accent-bright); + --brand-value: var(--accent-bright); + --link-value: var(--accent-bright); + --link-hover-value: var(--accent-brighter); + --scrim-value: oklch(10% 0.01 270 / 60%); + --success-value: var(--status-canonical-bright); + --warning-value: var(--status-fork-bright); + --caution-value: var(--status-optimistic-bright); + --danger-value: oklch(68% 0.19 25); + --danger-surface-value: oklch(28% 0.07 25); + --danger-accent-value: oklch(70% 0.19 20); + --canonical-value: var(--status-canonical-bright); + --canonical-deep-value: var(--status-canonical-dim); + --canonical-edge-value: var(--status-canonical-shade); + --canonical-ring-value: var(--status-canonical-bright); + --fork-value: var(--status-fork-bright); + --fork-deep-value: var(--status-fork-dim); + --fork-edge-value: var(--status-fork-shade); + --fork-ring-value: var(--status-fork-bright); + --finalized-value: var(--status-finalized-bright); + --finalized-deep-value: var(--status-finalized-dim); + --finalized-edge-value: var(--status-finalized-shade); + --finalized-ring-value: var(--status-finalized-bright); + --justified-value: var(--status-justified-bright); + --justified-deep-value: var(--status-justified-dim); + --justified-edge-value: var(--status-justified-shade); + --justified-ring-value: var(--status-justified-bright); + --invalid-value: var(--status-invalid-bright); + --invalid-deep-value: var(--status-invalid-dim); + --invalid-edge-value: var(--status-invalid-shade); + --invalid-ring-value: var(--status-invalid-bright); + --optimistic-value: var(--status-optimistic-bright); + --optimistic-deep-value: var(--status-optimistic-dim); + --optimistic-edge-value: var(--status-optimistic-shade); + --optimistic-ring-value: var(--status-optimistic-bright); + --marker-value: var(--status-fork-bright); + --marker-active-value: oklch(80% 0.16 70); + --marker-time-value: oklch(55% 0.09 235); + --marker-time-active-value: var(--accent-bright); + --scrubber-value: oklch(72% 0.01 270); + --scrubber-strong-value: var(--ink-200); + --rec-value: var(--rose-deep); + --rec-strong-value: var(--rose-mid); + --tick-value: oklch(100% 0 0 / 28%); + --tick-label-value: var(--ink-500); + --column-fade-mid: oklch(100% 0 0 / 12%); + --grid-dot-color: oklch(100% 0 0 / 6%); + --glass-highlight: oklch(100% 0 0 / 7%); + } +} diff --git a/web/src/index.tsx b/web/src/index.tsx index 352b2e9..b26d2cb 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,45 +1,37 @@ -import './styles/global.css'; +import './index.css'; import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createRouter, RouterProvider } from '@tanstack/react-router'; import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en.json'; import ReactDOM from 'react-dom/client'; -import { Route, Switch } from 'wouter'; -import App from '@app/App'; import ErrorBoundary from '@app/ErrorBoundary'; +import { routeTree } from './routeTree.gen'; + const queryClient = new QueryClient(); TimeAgo.addDefaultLocale(en); -// @ts-expect-error ignore +const router = createRouter({ routeTree }); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} + if (process.env.NODE_ENV === 'development' && import.meta.env.VITE_MOCK) { const { worker } = await import('@app/mocks/browser'); - worker.start({ - waitUntilReady: true, - }); + await worker.start(); } ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - {() => } - - {params => { - const pathParts = params['*'].split('/'); - const hasEvents = pathParts[pathParts.length - 1] === 'events'; - const nodePath = hasEvents ? pathParts.slice(0, -1).join('/') : params['*']; - return ; - }} - - {({ id }) => } - - - - + , diff --git a/web/src/mocks/handlers.ts b/web/src/mocks/handlers.ts index 73965c7..6db8424 100644 --- a/web/src/mocks/handlers.ts +++ b/web/src/mocks/handlers.ts @@ -1,25 +1,26 @@ import { http, RequestHandler, HttpResponse } from 'msw'; -import { - Response, - V1GetEthereumNowResponse, - V1GetEthereumSpecResponse, - V1MetadataListResponse, - V1MetadataListNodesResponse, - V1GetFrameResponse, -} from '@app/types/api'; +import type { + EthereumNow, + EthereumSpec, + GetEthereumNowResponse, + GetEthereumSpecResponse, + GetFrameResponse, + ListMetadataNodesResponse, + ListMetadataResponse, +} from '@api'; import { generateRandomForkChoiceData } from '@utils/api'; import { BASE_URL } from '@utils/environment'; export const networkName = 'goerli'; -export const spec: Required['spec'] = { +export const spec: EthereumSpec = { seconds_per_slot: 12, slots_per_epoch: 32, genesis_time: '2021-03-23T14:00:00Z', }; -export function getNow(): Required { +export function getNow(): EthereumNow { const slot = Math.floor( (Date.now() - new Date(spec.genesis_time).getTime()) / 1000 / spec.seconds_per_slot, ); @@ -30,24 +31,26 @@ export function getNow(): Required { }; } -export const nodes: Required = { +export const nodes: ListMetadataNodesResponse['data'] = { nodes: ['ams3-teku-001', 'syd1-lighthouse-001', 'syd1-prysm-001'], pagination: { total: 3 }, }; export const handlers: Array = [ http.get(`${BASE_URL}api/v1/ethereum/now`, async () => { - return HttpResponse.json({ data: getNow() }); + return HttpResponse.json({ data: getNow() } satisfies GetEthereumNowResponse); }), http.get(`${BASE_URL}api/v1/ethereum/spec`, () => { - return HttpResponse.json({ data: { network_name: networkName, spec } }); + return HttpResponse.json({ + data: { network_name: networkName, spec }, + } satisfies GetEthereumSpecResponse); }), http.post(`${BASE_URL}api/v1/metadata/nodes`, () => { - return HttpResponse.json({ data: nodes }); + return HttpResponse.json({ data: nodes } satisfies ListMetadataNodesResponse); }), http.post(`${BASE_URL}api/v1/metadata`, async () => { const { slot, epoch } = getNow(); - const data: Response = { + const data: ListMetadataResponse = { data: { frames: [ { @@ -59,6 +62,8 @@ export const handlers: Array = [ wall_clock_slot: slot, wall_clock_epoch: epoch, labels: [], + consensus_client: 'teku', + event_source: 'beacon_node', }, ], pagination: { total: 1 }, @@ -67,9 +72,9 @@ export const handlers: Array = [ return HttpResponse.json(data); }), http.get(`${BASE_URL}api/v1/frames/:id`, ({ params }) => { - const id = Array.isArray(params.id) ? params.id[0] : params.id; + const id = (Array.isArray(params.id) ? params.id[0] : params.id) ?? 'unknown'; const { slot, epoch } = getNow(); - const data: Response = { + const data: GetFrameResponse = { data: { frame: { data: generateRandomForkChoiceData(), @@ -82,6 +87,8 @@ export const handlers: Array = [ wall_clock_slot: slot, wall_clock_epoch: epoch, labels: [], + consensus_client: 'teku', + event_source: 'beacon_node', }, }, }, diff --git a/web/src/parts/BYOFooter.stories.tsx b/web/src/parts/BYOFooter.stories.tsx new file mode 100644 index 0000000..7414108 --- /dev/null +++ b/web/src/parts/BYOFooter.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import { expect, userEvent, within } from 'storybook/test'; + +import useFocus from '@contexts/focus'; +import BYOFooter from '@parts/BYOFooter'; +import { storyFrames } from '@app/stories/fixtures'; +import { withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Parts/BYO Footer', + component: BYOFooter, + parameters: { + layout: 'fullscreen', + tanstack: { + router: { + path: '/byo', + }, + }, + }, + decorators: [ + withForkyProviders({ + focus: { + byo: true, + }, + }), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const alphaNodeCount = storyFrames.alpha.data.fork_choice_nodes.length; + +function BYOFooterHarness() { + const { byoData } = useFocus(); + + return ( +
+
+ + {byoData + ? `Loaded ${byoData.frame.data.fork_choice_nodes.length} nodes` + : 'No BYO data loaded'} + +
+ +
+ ); +} + +export const Idle: Story = { + render: () => , +}; + +export const Dragging: Story = { + render: () => , + play: async ({ canvasElement }) => { + canvasElement.ownerDocument.dispatchEvent( + new DragEvent('dragover', { bubbles: true, cancelable: true }), + ); + const canvas = within(canvasElement); + await expect(await canvas.findByText('Drop to view')).toBeVisible(); + }, +}; + +export const InvalidJsonUpload: Story = { + render: () => , + play: async ({ canvasElement }) => { + const input = canvasElement.querySelector('input[type="file"]'); + if (!(input instanceof HTMLInputElement)) throw new Error('File input not found'); + await userEvent.upload(input, new File(['not json'], 'fork-choice.json')); + const canvas = within(canvasElement); + await expect(await canvas.findByText('The file is not valid JSON.')).toBeVisible(); + }, +}; + +export const ValidJsonUpload: Story = { + render: () => , + play: async ({ canvasElement }) => { + const input = canvasElement.querySelector('input[type="file"]'); + if (!(input instanceof HTMLInputElement)) throw new Error('File input not found'); + await userEvent.upload( + input, + new File([JSON.stringify(storyFrames.alpha.data)], 'fork-choice.json', { + type: 'application/json', + }), + ); + const canvas = within(canvasElement); + await expect( + await canvas.findByText(new RegExp(`loaded ${alphaNodeCount} nodes`, 'i')), + ).toBeVisible(); + }, +}; diff --git a/web/src/parts/BYOFooter.tsx b/web/src/parts/BYOFooter.tsx index 3bdec89..d9cfb97 100644 --- a/web/src/parts/BYOFooter.tsx +++ b/web/src/parts/BYOFooter.tsx @@ -1,8 +1,9 @@ import { memo, useRef, useState, useEffect, useCallback } from 'react'; import { DocumentArrowUpIcon } from '@heroicons/react/24/solid'; +import classNames from 'clsx'; -import { ForkChoiceData, Frame, Checkpoint, ForkChoiceNode } from '@app/types/api'; +import { ForkChoice, Frame, Checkpoint, ForkChoiceNode } from '@api'; import useFocus from '@contexts/focus'; import { processForkChoiceData } from '@utils/graph'; @@ -55,7 +56,7 @@ function validateForkChoiceData(data: unknown): { isValid: boolean; message?: st return { isValid: false, message: 'Data is not an object or is null.' }; } - const castedData = data as ForkChoiceData; + const castedData = data as ForkChoice; if (!castedData.fork_choice_nodes) { return { isValid: false, message: 'fork_choice_nodes is missing.' }; @@ -114,6 +115,9 @@ function BYOFooter() { fetched_at: new Date().toISOString(), wall_clock_slot: 0, wall_clock_epoch: 0, + labels: [], + consensus_client: '', + event_source: 'unknown', }, data: jsonData, }; @@ -187,31 +191,41 @@ function BYOFooter() { }; return ( -
+
-
- -
-
+ setLocation(closeTo)}> + +
+
+
+ +
+
+
+ + + Events + + {node && ( + + {node} + + + + + )} + +
+
-
- -
- {queryOptions.map(option => ( - - classNames( - 'cursor-pointer focus:outline-none', - focus - ? 'ring-2 ring-stone-600 dark:ring-stone-400 ring-offset-2' - : '', - checked - ? 'bg-stone-600 dark:bg-stone-400 text-stone-100 dark:text-stone-900 hover:bg-stone-500' - : 'ring-1 ring-inset ring-stone-300 dark:ring-stone-700 bg-stone-200 dark:bg-stone-800 text-stone-900 dark:text-stone-100 hover:bg-stone-50 dark:hover:bg-stone-950', - 'flex items-center justify-center rounded-md py-3 px-3 text-sm font-semibold uppercase sm:flex-1', - ) - } - > - - - ))} -
-
- {queryType.name === 'Relative' && ( - - {({ open }) => ( - <> -
- - {queryRelative.name} - - - +
+
+
+ { + setQueryType(value); + updateQueryFilter(value, queryRelative, querySlot, queryEpoch); + }} + > +
+ {queryOptions.map(option => ( + + classNames( + 'cursor-pointer focus:outline-hidden transition-colors duration-150', + focus + ? 'ring-2 ring-active ring-offset-2 ring-offset-surface' + : '', + checked + ? 'bg-active text-foreground-inverted hover:bg-active-hover' + : 'ring-1 ring-inset ring-border-strong bg-surface-raised text-muted hover:bg-surface-hover hover:text-foreground', + 'flex items-center justify-center rounded-lg py-2.5 px-3 font-mono text-xs font-semibold uppercase tracking-wider sm:flex-1', + ) + } + > + + + ))} +
+
+ {queryType.name === 'Relative' && ( + { + setQueryRelative(value); + updateQueryFilter(queryType, value, querySlot, queryEpoch); + }} + > + {() => ( + <> +
+ + {queryRelative.name} + + + - + {queryRelativeOptions.map(option => ( + + classNames( + focus + ? 'bg-active text-foreground-inverted' + : 'text-foreground', + 'relative cursor-default select-none py-2 pl-8 pr-4', + ) + } + value={option} > - - {queryRelativeOptions.map(option => ( - - classNames( - focus - ? 'bg-stone-600 dark:bg-stone-400 text-stone-100 dark:text-stone-900' - : 'text-stone-900 dark:text-stone-100', - 'relative cursor-default select-none py-2 pl-8 pr-4', - ) - } - value={option} + {({ selected, focus }) => ( + <> + - {({ selected, focus }) => ( - <> - - {option.name} - + {option.name} + - {selected ? ( - - - ) : null} - - )} - - ))} - - -
- - )} -
+ {selected ? ( + + + ) : null} + + )} + + ))} + +
+ )} + + )} - {queryType.name === 'Slot' && ( -
- setQuerySlot(value)} - type="text" - /> -
- )} + {queryType.name === 'Slot' && ( +
+ { + setQuerySlot(value); + updateQueryFilter(queryType, queryRelative, value, queryEpoch); + }} + type="text" + /> +
+ )} - {queryType.name === 'Epoch' && ( -
- setQueryEpoch(value)} - type="text" - /> -
- )} + {queryType.name === 'Epoch' && ( +
+ { + setQueryEpoch(value); + updateQueryFilter(queryType, queryRelative, querySlot, value); + }} + type="text" + />
-
-
- - - - - - - - - - + + + + ); + })} + +
- Date - - Type - - Node - - Snapshot ID - - Slot - - Epoch - +
+
+ + + + + + + + + + + + + + {events.map(event => { + if (!event) return null; + const time = new Date(event.time); + const timeDiff = (time.getTime() - genesisTime) / 1000; + const slot = Math.floor(timeDiff / secondsPerSlot); + const epoch = Math.floor(slot / slotsPerEpoch); + return ( + + + - - - {events.map(event => { - if (!event) return null; - const time = new Date(event.time); - const timeDiff = (time.getTime() - genesisTime) / 1000; - const slot = Math.floor(timeDiff / secondsPerSlot); - const epoch = Math.floor(slot / slotsPerEpoch); - return ( - - - - + + - - - - - - ); - })} - -
+ Date + + Type + + Node + + Snapshot ID + + Slot + + Epoch + + Extra Data +
+ + + {time.toISOString()} + + - Extra Data - -
- - ({time.toISOString()}) - - {event.type} - + {event.type} + + { + stop(); + setTime(time.getTime() + 100); + }} + > + {event.node} + + + + {event.snapshots.map(({ id, key }) => ( +
+ {key && {key}:} { stop(); setTime(time.getTime() + 100); }} > - {event.node} + {id} + - -
- {event.snapshots.map(({ id, key }) => ( -
- {key && ( - {key}: - )} - { - stop(); - setTime(time.getTime() + 100); - }} - > - {id} - - -
- ))} -
- {slot} - - {epoch} - - {event.extraData.map(([key, value, color]) => { - return ( -
- {key}: {value} -
- ); - })} -
- {(isLoading || Boolean(error)) && ( - - )} - {events.length === 0 && !isLoading && !error && ( -
- No events found -
- )} +
+ ))} + +
+ {slot} + + {epoch} + + {event.extraData.map(([key, value, color]) => { + return ( +
+ {key}: {value} +
+ ); + })} +
+ {(isLoading || Boolean(error)) && ( + + )} + {events.length === 0 && !isLoading && !error && ( +
+ No events found
-
+ )}
- - -
+
+
+
-
- +
+
); diff --git a/web/src/parts/FrameFooter.stories.tsx b/web/src/parts/FrameFooter.stories.tsx new file mode 100644 index 0000000..704e894 --- /dev/null +++ b/web/src/parts/FrameFooter.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import FrameFooter from '@parts/FrameFooter'; +import { storyApiHandlers, storyFrameIds } from '@app/stories/fixtures'; +import { withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Parts/Frame Footer', + component: FrameFooter, + parameters: { + layout: 'fullscreen', + msw: { + handlers: storyApiHandlers(), + }, + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const WithoutActiveFrame: Story = { + render: () => ( +
+ +
+ ), +}; + +export const WithActiveFrame: Story = { + decorators: [ + withForkyProviders({ + focus: { + frameId: storyFrameIds[0], + }, + }), + ], + render: () => ( +
+ +
+ ), +}; diff --git a/web/src/parts/FrameFooter.tsx b/web/src/parts/FrameFooter.tsx index e4da499..cf93860 100644 --- a/web/src/parts/FrameFooter.tsx +++ b/web/src/parts/FrameFooter.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { RectangleGroupIcon, RectangleStackIcon } from '@heroicons/react/24/solid'; -import { Link } from 'wouter'; +import Link from '@components/Link'; import Download from '@components/Download'; import useAction from '@hooks/useActive'; @@ -12,30 +12,33 @@ function FrameFooter() { const { data } = useFrameQuery(ids[0], ids.length > 0); return ( -
-
- - - +
+
+ + + Aggregated View {data && ( <> - - - + + + Source View - + Download diff --git a/web/src/parts/Graph.stories.tsx b/web/src/parts/Graph.stories.tsx new file mode 100644 index 0000000..fab206a --- /dev/null +++ b/web/src/parts/Graph.stories.tsx @@ -0,0 +1,494 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import { expect, userEvent, within } from 'storybook/test'; + +import type { ForkChoice, ForkChoiceNode, Frame, FrameMetadata } from '@api'; +import type { ProcessedData } from '@app/types/graph'; +import Graph from '@parts/Graph'; +import { storySpec } from '@app/stories/fixtures'; +import { withForkyProviders } from '@app/stories/storybook'; +import { processForkChoiceData } from '@utils/graph'; + +const graphNodes = [ + { node: 'ams3-teku-001', consensusClient: 'teku', region: 'ams3' }, + { node: 'syd1-lighthouse-001', consensusClient: 'lighthouse', region: 'syd1' }, + { node: 'sfo2-prysm-001', consensusClient: 'prysm', region: 'sfo2' }, +]; + +const graphGenesisTime = new Date(storySpec.genesis_time).getTime(); + +function blockRoot(key: string): string { + let hash = 0; + for (let i = 0; i < key.length; i += 1) { + hash = (hash * 31 + key.charCodeAt(i)) >>> 0; + } + return `0x${hash.toString(16).padStart(64, '0')}`; +} + +function fetchedAt(slot: number, nodeIndex: number): string { + return new Date( + graphGenesisTime + slot * storySpec.seconds_per_slot * 1000 + (nodeIndex + 1) * 1_000, + ).toISOString(); +} + +interface GraphNodeSpec { + key: string; + slot: number; + parent?: string; + weight: number; + validity?: ForkChoiceNode['validity']; + graffiti?: string; +} + +function forkChoiceNode({ + key, + slot, + parent, + weight, + validity = 'valid', + graffiti, +}: GraphNodeSpec): ForkChoiceNode { + return { + slot: slot.toString(), + block_root: blockRoot(key), + parent_root: blockRoot(parent ?? 'genesis'), + justified_epoch: '3', + finalized_epoch: '3', + weight: weight.toString(), + validity, + execution_block_hash: blockRoot(`execution-${key}-${weight}`), + extra_data: { + graffiti: graffiti ?? key, + proposer_boost_root: blockRoot(key), + unrealized_justified_root: blockRoot('canonical-103'), + unrealized_finalized_root: blockRoot('canonical-100'), + }, + }; +} + +function forkChoice(nodes: GraphNodeSpec[]): ForkChoice { + return { + finalized_checkpoint: { epoch: '3', root: blockRoot('canonical-100') }, + justified_checkpoint: { epoch: '3', root: blockRoot('canonical-103') }, + fork_choice_nodes: nodes.map(forkChoiceNode), + }; +} + +function frameMetadata({ + frameId, + nodeIndex, + slot, + scenario, +}: { + frameId: string; + nodeIndex: number; + slot: number; + scenario: string; +}): FrameMetadata { + const source = graphNodes[nodeIndex]; + return { + id: frameId, + node: source.node, + fetched_at: fetchedAt(slot, nodeIndex), + wall_clock_slot: slot, + wall_clock_epoch: Math.floor(slot / storySpec.slots_per_epoch), + labels: [`region=${source.region}`, `story_graph_scenario=${scenario}`], + consensus_client: source.consensusClient, + event_source: 'beacon_node', + }; +} + +function processedFrame({ + scenario, + frameId, + nodeIndex, + nodes, +}: { + scenario: string; + frameId: string; + nodeIndex: number; + nodes: GraphNodeSpec[]; +}): ProcessedData { + const maxSlot = Math.max(...nodes.map(({ slot }) => slot)); + const frame: Required = { + metadata: frameMetadata({ frameId, nodeIndex, slot: maxSlot, scenario }), + data: forkChoice(nodes), + }; + return processForkChoiceData(frame); +} + +function adjustWeights(nodes: GraphNodeSpec[], nodeIndex: number): GraphNodeSpec[] { + const multiplier = [1, 0.94, 0.88][nodeIndex] ?? 1; + return nodes.map(node => ({ + ...node, + weight: Math.max(1, Math.round(node.weight * multiplier)), + })); +} + +function scenarioFrames(scenario: string, nodes: GraphNodeSpec[], count = graphNodes.length) { + return Array.from({ length: count }, (_, nodeIndex) => + processedFrame({ + scenario, + frameId: `${scenario}-${nodeIndex + 1}`, + nodeIndex, + nodes: adjustWeights(nodes, nodeIndex), + }), + ); +} + +function idsFor(data: ProcessedData[]): string[] { + return data.map(({ frame }) => frame.metadata.id); +} + +function single(data: ProcessedData[]): ProcessedData[] { + return [data[0]]; +} + +const graphScenarios = { + happyPath: scenarioFrames('happy-path', [ + { key: 'canonical-100', slot: 100, weight: 1_000, graffiti: 'finalized checkpoint' }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-102', slot: 102, parent: 'canonical-101', weight: 960 }, + { key: 'canonical-103', slot: 103, parent: 'canonical-102', weight: 940 }, + { key: 'canonical-104', slot: 104, parent: 'canonical-103', weight: 920 }, + { key: 'canonical-105', slot: 105, parent: 'canonical-104', weight: 900 }, + { key: 'canonical-106', slot: 106, parent: 'canonical-105', weight: 880 }, + { key: 'canonical-107', slot: 107, parent: 'canonical-106', weight: 860 }, + { key: 'canonical-108', slot: 108, parent: 'canonical-107', weight: 840 }, + ]), + missedSlots: scenarioFrames('missed-slots', [ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-103', slot: 103, parent: 'canonical-101', weight: 950 }, + { key: 'canonical-106', slot: 106, parent: 'canonical-103', weight: 920 }, + { key: 'canonical-107', slot: 107, parent: 'canonical-106', weight: 900 }, + { key: 'canonical-110', slot: 110, parent: 'canonical-107', weight: 870 }, + ]), + previousForkContinued: scenarioFrames('previous-fork-continued', [ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-102', slot: 102, parent: 'canonical-101', weight: 960 }, + { key: 'short-fork-103', slot: 103, parent: 'canonical-102', weight: 360 }, + { key: 'canonical-104', slot: 104, parent: 'canonical-102', weight: 930 }, + { key: 'canonical-105', slot: 105, parent: 'canonical-104', weight: 910 }, + { key: 'canonical-106', slot: 106, parent: 'canonical-105', weight: 890 }, + { key: 'canonical-107', slot: 107, parent: 'canonical-106', weight: 870 }, + ]), + headForkLowerWeight: scenarioFrames('head-fork-lower-weight', [ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-102', slot: 102, parent: 'canonical-101', weight: 960 }, + { key: 'canonical-103', slot: 103, parent: 'canonical-102', weight: 940 }, + { key: 'canonical-104', slot: 104, parent: 'canonical-103', weight: 920 }, + { key: 'canonical-106', slot: 106, parent: 'canonical-104', weight: 900 }, + { key: 'canonical-107', slot: 107, parent: 'canonical-106', weight: 870 }, + { + key: 'head-fork-109', + slot: 109, + parent: 'canonical-106', + weight: 260, + graffiti: 'head fork with lower weight', + }, + ]), + multiForks: scenarioFrames('multi-forks', [ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-102', slot: 102, parent: 'canonical-101', weight: 960 }, + { key: 'fork-103', slot: 103, parent: 'canonical-101', weight: 390 }, + { key: 'canonical-104', slot: 104, parent: 'canonical-102', weight: 930 }, + { key: 'fork-105', slot: 105, parent: 'canonical-104', weight: 430 }, + { key: 'canonical-106', slot: 106, parent: 'canonical-104', weight: 900 }, + { key: 'canonical-108', slot: 108, parent: 'canonical-106', weight: 870 }, + { key: 'fork-109', slot: 109, parent: 'canonical-106', weight: 350 }, + { key: 'canonical-110', slot: 110, parent: 'canonical-108', weight: 840 }, + ]), + chaosForks: scenarioFrames('chaos-forks', [ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'early-fork-101', slot: 101, parent: 'canonical-100', weight: 280 }, + { key: 'canonical-102', slot: 102, parent: 'canonical-100', weight: 960 }, + { key: 'canonical-103', slot: 103, parent: 'canonical-102', weight: 940 }, + { key: 'canonical-104', slot: 104, parent: 'canonical-103', weight: 920 }, + { key: 'side-fork-105', slot: 105, parent: 'canonical-102', weight: 470 }, + { + key: 'invalid-fork-106', + slot: 106, + parent: 'canonical-104', + weight: 120, + validity: 'invalid', + graffiti: 'invalid payload branch', + }, + { key: 'canonical-107', slot: 107, parent: 'canonical-104', weight: 890 }, + { + key: 'optimistic-fork-109', + slot: 109, + parent: 'side-fork-105', + weight: 430, + validity: 'optimistic', + graffiti: 'optimistic side branch', + }, + { + key: 'detached-110', + slot: 110, + parent: 'unknown-parent', + weight: 80, + graffiti: 'missing parent', + }, + { key: 'canonical-111', slot: 111, parent: 'canonical-107', weight: 850 }, + { key: 'late-fork-112', slot: 112, parent: 'canonical-107', weight: 310 }, + ]), + rareSameSlot: scenarioFrames('rare-same-slot-siblings', [ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-102', slot: 102, parent: 'canonical-101', weight: 960 }, + { key: 'canonical-103', slot: 103, parent: 'canonical-102', weight: 930 }, + { + key: 'same-slot-fork-103', + slot: 103, + parent: 'canonical-102', + weight: 390, + graffiti: 'rare same-slot sibling', + }, + { key: 'canonical-104', slot: 104, parent: 'canonical-103', weight: 910 }, + { key: 'canonical-105', slot: 105, parent: 'canonical-104', weight: 890 }, + ]), + // Spans far more than MAX_GRAPH_SLOTS (128): the tail is truncated and the + // finalized (slot 100) + justified (slot 103) checkpoints are pinned into + // compact columns left of the window, bridged by "slots hidden" markers. + longTail: scenarioFrames('long-tail', [ + { key: 'canonical-100', slot: 100, weight: 1_000, graffiti: 'finalized checkpoint' }, + { key: 'canonical-103', slot: 103, parent: 'canonical-100', weight: 990 }, + ...Array.from({ length: 16 }, (_, i) => ({ + key: `canonical-${273 + i * 8}`, + slot: 273 + i * 8, + parent: i === 0 ? 'canonical-103' : `canonical-${273 + (i - 1) * 8}`, + weight: 980 - i * 8, + })), + { key: 'fork-396', slot: 396, parent: 'canonical-393', weight: 310 }, + { key: 'canonical-400', slot: 400, parent: 'canonical-393', weight: 850 }, + ]), +}; + +// A stuck/syncing source: its fork-choice head (slot 104) sits ~1.1k slots +// behind its own wall clock. Stage partitions frames like this out of the +// aggregation; the Sources panel lists them struck through with their lag. +const stuckNodeFrame: ProcessedData = processForkChoiceData({ + metadata: { + id: 'stuck-node-1', + node: 'fra1-nimbus-001', + fetched_at: fetchedAt(1208, 0), + wall_clock_slot: 1208, + wall_clock_epoch: Math.floor(1208 / storySpec.slots_per_epoch), + labels: ['region=fra1', 'story_graph_scenario=stuck-node'], + consensus_client: 'nimbus', + event_source: 'beacon_node', + }, + data: forkChoice([ + { key: 'canonical-100', slot: 100, weight: 1_000 }, + { key: 'canonical-101', slot: 101, parent: 'canonical-100', weight: 980 }, + { key: 'canonical-103', slot: 103, parent: 'canonical-101', weight: 960 }, + { key: 'canonical-104', slot: 104, parent: 'canonical-103', weight: 940 }, + ]), +}); + +const meta = { + title: 'Parts/Graph', + component: Graph, + parameters: { + layout: 'fullscreen', + }, + decorators: [withForkyProviders()], + render: args => ( +
+ +
+ ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +function singleNodeStory(data: ProcessedData[], unique: string): Story { + return { + parameters: { + tanstack: { + router: { + path: '/node/$', + params: { _splat: data[0].frame.metadata.node }, + }, + }, + }, + args: { + data: single(data), + ids: [data[0].frame.metadata.id], + unique: `single-${unique}`, + }, + }; +} + +function aggregatedStory(data: ProcessedData[], unique: string): Story { + return { + parameters: { + tanstack: { + router: { + path: '/', + }, + }, + }, + args: { + data, + ids: idsFor(data), + unique: `aggregated-${unique}`, + }, + }; +} + +export const Empty: Story = { + parameters: { + tanstack: { + router: { + path: '/', + }, + }, + }, + args: { + data: [], + ids: [], + unique: 'empty', + }, +}; + +export const SingleNodeHappyPathAllSlots = singleNodeStory(graphScenarios.happyPath, 'happy-path'); +export const AggregatedHappyPathAllSlots = aggregatedStory(graphScenarios.happyPath, 'happy-path'); + +export const SingleNodeHappyPathMissedSlots = singleNodeStory( + graphScenarios.missedSlots, + 'missed-slots', +); +export const AggregatedHappyPathMissedSlots = aggregatedStory( + graphScenarios.missedSlots, + 'missed-slots', +); + +export const SingleNodePreviousForkContinued = singleNodeStory( + graphScenarios.previousForkContinued, + 'previous-fork-continued', +); +export const AggregatedPreviousForkContinued = aggregatedStory( + graphScenarios.previousForkContinued, + 'previous-fork-continued', +); + +export const SingleNodeHeadForkLowerWeight = singleNodeStory( + graphScenarios.headForkLowerWeight, + 'head-fork-lower-weight', +); +export const AggregatedHeadForkLowerWeight = aggregatedStory( + graphScenarios.headForkLowerWeight, + 'head-fork-lower-weight', +); + +export const SingleNodeMultiForks = singleNodeStory(graphScenarios.multiForks, 'multi-forks'); +export const AggregatedMultiForks = aggregatedStory(graphScenarios.multiForks, 'multi-forks'); + +export const SingleNodeChaosForks = singleNodeStory(graphScenarios.chaosForks, 'chaos-forks'); +export const AggregatedChaosForks = aggregatedStory(graphScenarios.chaosForks, 'chaos-forks'); + +export const SingleNodeRareSameSlotSiblings = singleNodeStory( + graphScenarios.rareSameSlot, + 'rare-same-slot-siblings', +); +export const AggregatedRareSameSlotSiblings = aggregatedStory( + graphScenarios.rareSameSlot, + 'rare-same-slot-siblings', +); + +export const AggregatedSummaryCollapsed: Story = { + ...aggregatedStory(graphScenarios.happyPath, 'summary-collapsed'), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole('button', { name: 'Collapse sources' })); + await expect(canvas.getByRole('button', { name: 'Show sources' })).toBeVisible(); + }, +}; + +/** + * The graph auto-focuses the head on mount, which puts the truncated tail + * (pinned checkpoints + gap markers) tens of thousands of canvas pixels off + * screen to the left. Pan there with real pointer events so the zoom/pan + * library's state stays consistent and the story remains interactive. + */ +async function panToTruncatedTail(canvasElement: HTMLElement) { + // wait for the graph to mount and zoom to the head + await new Promise(resolve => setTimeout(resolve, 400)); + const wrapper = canvasElement.querySelector('.react-transform-wrapper'); + const content = canvasElement.querySelector('.react-transform-component'); + if (!wrapper || !content) return; + + const matrix = new DOMMatrixReadOnly(getComputedStyle(content).transform); + const scale = matrix.a || 1; + const rect = wrapper.getBoundingClientRect(); + // frame the pinned finalized checkpoint (world x ≈ -1800) near the left + // edge and the chain rail (world y ≈ -450) vertically centered + const deltaX = 80 - (matrix.e + -1800 * scale); + const deltaY = rect.height / 2 - (matrix.f + -450 * scale); + + const startX = rect.left + rect.width / 2; + const startY = rect.top + rect.height / 2; + const mouse = { bubbles: true, cancelable: true, button: 0 }; + // react-zoom-pan-pinch pans via mouse events (mousedown on the wrapper, + // mousemove/mouseup on the document) + wrapper.dispatchEvent( + new MouseEvent('mousedown', { ...mouse, clientX: startX, clientY: startY }), + ); + const steps = 8; + for (let i = 1; i <= steps; i++) { + document.dispatchEvent( + new MouseEvent('mousemove', { + ...mouse, + clientX: startX + (deltaX * i) / steps, + clientY: startY + (deltaY * i) / steps, + }), + ); + await new Promise(resolve => setTimeout(resolve, 25)); + } + document.dispatchEvent( + new MouseEvent('mouseup', { + ...mouse, + clientX: startX + deltaX, + clientY: startY + deltaY, + }), + ); +} + +export const SingleNodeTruncatedLongTail: Story = { + ...singleNodeStory(graphScenarios.longTail, 'truncated-long-tail'), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + // finalized→justified gap (3 slots) and justified→window gap (169 slots) + await expect(canvas.getAllByText(/slots\s+hidden/i)).toHaveLength(2); + await expect(canvas.getByText('169')).toBeVisible(); + await panToTruncatedTail(canvasElement); + }, +}; + +export const AggregatedTruncatedLongTail: Story = { + ...aggregatedStory(graphScenarios.longTail, 'truncated-long-tail'), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getAllByText(/slots\s+hidden/i)).toHaveLength(2); + await panToTruncatedTail(canvasElement); + }, +}; + +export const AggregatedWithStuckNode: Story = { + ...aggregatedStory(graphScenarios.happyPath, 'stuck-node'), + args: { + ...aggregatedStory(graphScenarios.happyPath, 'stuck-node').args, + behind: [stuckNodeFrame], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByText('1104 slots behind')).toBeVisible(); + await expect(canvas.getByText('fra1-nimbus-001')).toBeVisible(); + }, +}; diff --git a/web/src/parts/Graph.tsx b/web/src/parts/Graph.tsx index a379240..4268792 100644 --- a/web/src/parts/Graph.tsx +++ b/web/src/parts/Graph.tsx @@ -10,7 +10,8 @@ import { } from '@heroicons/react/24/solid'; import classNames from 'clsx'; import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'; -import { useLocation, Link } from 'wouter'; +import Link from '@components/Link'; +import { useAppNavigate, usePathname } from '@hooks/useAppNavigation'; import SlotBoundary from '@app/components/SlotBoundary'; import { AggregatedNodeAttributes, ProcessedData, WeightedNodeAttributes } from '@app/types/graph'; @@ -47,7 +48,8 @@ function Graph({ ids: string[]; unique: string; }) { - const [location, navigate] = useLocation(); + const location = usePathname(); + const navigate = useAppNavigate(); const { setFrameId, setAggregatedFrameIds, setFrameBlock, setAggregatedFramesBlock } = useSelection(); const { slotsPerEpoch } = useEthereum(); @@ -61,17 +63,11 @@ function Graph({ const isBYO = location.startsWith('/byo'); const [windowWidth, windowHeight] = useWindowSize(); - const [scaleMultiplier, setScaleMultiplier] = useState( - calculateScaleMultiplier(windowWidth, windowHeight), - ); + const scaleMultiplier = calculateScaleMultiplier(windowWidth, windowHeight); const [scale, setScale] = useState(scaleMultiplier); const [focused, setFocused] = useState(true); const [isSummaryCollapsed, setIsSummaryCollapsed] = useState(false); - useEffect(() => { - setScaleMultiplier(calculateScaleMultiplier(windowWidth, windowHeight)); - }, [windowHeight, windowWidth]); - useEffect(() => { if (focused) ref.current?.zoomToElement('head', scaleMultiplier); }, [nodes, focused, scaleMultiplier]); @@ -90,12 +86,12 @@ function Graph({ key={slot} slot={slot} epoch={isEpoch ? slot / slotsPerEpoch : undefined} - width={8} + width={4} height={1800} x={SPACING_X - RADIUS / 2 + index * SPACING_X} y={-SPACING_Y + RADIUS - 1800 / 2} textOffset={SPACING_Y / 2 - RADIUS / 1.5} - className="bg-gradient-to-t from-stone-100 dark:from-stone-900 from-10% dark:from-10% via-stone-500 dark:via-stone-500 via-50% dark:via-50% to-stone-100 dark:to-stone-900 to-90% dark:to-90%" + className="column-fade" /> ); }); @@ -136,17 +132,19 @@ function Graph({ {frame.metadata.node} - + {isBehind ? ( {slotsBehind} slots behind @@ -155,10 +153,8 @@ function Graph({ {truncateHash(weightedHead?.blockRoot)} @@ -180,8 +176,8 @@ function Graph({ y1={edge.source.y + RADIUS} x2={edge.target.x + RADIUS} y2={edge.target.y + RADIUS} - className="dark:bg-stone-50 bg-stone-700" - thickness={12} + className="bg-edge" + thickness={8} /> )); @@ -283,71 +279,77 @@ function Graph({ <> {location.startsWith('/node/') && ( )} {!isBYO && type === 'weighted' && ( )} {!isBYO && type === 'aggregated' && formattedSummary.length && ( -
+
{isSummaryCollapsed ? ( ) : ( -
-
- Sources +
+
+ + Sources +
-
+
- {formattedSummary} + {formattedSummary}
)} @@ -387,22 +389,20 @@ function Graph({ onClick={handleFocus} title="Focus to the head of the canonical chain" className={classNames( - isBYO ? 'top-20' : 'top-36', - 'fixed z-10 right-6 lg:right-8 top-36 text-stone-700 dark:text-stone-300 cursor-pointer w-10 h-10 rounded-md transition hover:bg-stone-900/5 dark:hover:bg-white/5', + isBYO ? 'top-16' : 'top-[6.75rem]', + 'glass-chrome fixed right-4 z-10 flex size-9 cursor-pointer items-center justify-center rounded-lg border border-border text-muted shadow-md transition-colors duration-150 hover:text-foreground lg:right-6', )} > Focus to the head of the canonical chain {focused && ( <> - - - + + + )} - {!focused && ( - - )} + {!focused && } ( ))} {truncation?.checkpoints.map(checkpoint => { @@ -429,12 +429,12 @@ function Graph({ key={`checkpoint-${checkpoint.slot}`} slot={checkpoint.slot} epoch={isEpoch ? checkpoint.slot / slotsPerEpoch : undefined} - width={8} + width={4} height={1800} x={checkpoint.x - RADIUS / 2} y={-SPACING_Y + RADIUS - 1800 / 2} textOffset={SPACING_Y / 2 - RADIUS / 1.5} - className="bg-gradient-to-t from-stone-100 dark:from-stone-900 from-10% dark:from-10% via-stone-500 dark:via-stone-500 via-50% dark:via-50% to-stone-100 dark:to-stone-900 to-90% dark:to-90%" + className="column-fade" /> ); })} @@ -447,8 +447,8 @@ function Graph({ y1={truncation.rail.y1 + RADIUS} x2={truncation.rail.x2 + RADIUS} y2={truncation.rail.y2 + RADIUS} - className="dark:bg-stone-500 bg-stone-400" - thickness={12} + className="bg-edge" + thickness={8} /> )} {truncation.markers.map(marker => ( diff --git a/web/src/parts/Header.stories.tsx b/web/src/parts/Header.stories.tsx new file mode 100644 index 0000000..09c2d0e --- /dev/null +++ b/web/src/parts/Header.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; +import { expect, userEvent, within } from 'storybook/test'; + +import Header from '@parts/Header'; +import { withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Parts/Header', + component: Header, + parameters: { + layout: 'fullscreen', + tanstack: { + router: { + path: '/', + }, + }, + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+
+
+ ), +}; + +export const MenuOpen: Story = { + render: () => ( +
+
+
+ ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(canvas.getByRole('button', { name: /open menu/i })); + await expect(canvas.getByText('An Ethereum fork choice explorer')).toBeVisible(); + }, +}; diff --git a/web/src/parts/Header.tsx b/web/src/parts/Header.tsx index 85d9535..0cfba31 100644 --- a/web/src/parts/Header.tsx +++ b/web/src/parts/Header.tsx @@ -1,143 +1,135 @@ -import { Fragment, useState } from 'react'; +import { useState } from 'react'; -import { Dialog, Transition, TransitionChild, DialogPanel } from '@headlessui/react'; +import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'; import { Bars3Icon, XMarkIcon, CalendarDaysIcon, DocumentArrowUpIcon, } from '@heroicons/react/24/outline'; -import { Link, useLocation } from 'wouter'; +import Link from '@components/Link'; +import { usePathname } from '@hooks/useAppNavigation'; +import { eventsPathFor } from '@utils/routes'; import Walker from '@app/components/Walker'; -import LogoSmall from '@assets/forky_logo_small.png'; +import ForkyMarkSmall from '@assets/forky-mark-small.svg'; import { ModeToggle } from '@components/ModeToggle'; +import useEthereum from '@contexts/ethereum'; + +const ICON_BUTTON = + 'inline-flex size-9 items-center justify-center rounded-md text-muted transition-colors duration-150 hover:bg-overlay/5 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent'; export default function Header() { const [menuOpen, setMenuOpen] = useState(false); - const [location] = useLocation(); + const location = usePathname(); + const { networkName } = useEthereum(); return ( -
-
- - - - -
- -
-
-
- - -
- -
-
-
- +
+ + + +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+

+ Mission control for the Beacon Chain +

+

+ An Ethereum fork choice explorer +

+
+

+ About +

+

+ Forky captures, stores and + visualizes fork choice data from the Ethereum Beacon Chain.{' '} + Forky is designed to provide a live + view of the Ethereum network, along with historical access. +

+

+ While the Forky source code is + maintained by the Ethereum Foundation DevOps team, instances can be + operated by the community ❤️ +

+

+ If you'd like to run your own instance of{' '} + Forky, checkout out the{' '} + - Close menu -

+ Github repository + {' '} + for instructions. +

-
-
-
-

- An Ethereum fork choice explorer -

-
-

- About -

-

- Forky captures, stores and - visualizes fork choice data from the Ethereum Beacon Chain.{' '} - Forky is designed to provide - a live view of the Ethereum network, along with historical access. -

-

- While the Forky source code - is maintained by the Ethereum Foundation DevOps team, instances can - be operated by the community ❤️ -

-

- If you'd like to run your own instance of{' '} - Forky, checkout out the{' '} - - Github repository - {' '} - for instructions. -

-
-
-
-
- - +
+
-
+
-
- -
-
+
+
+ + ); } diff --git a/web/src/parts/Selection.stories.tsx b/web/src/parts/Selection.stories.tsx new file mode 100644 index 0000000..47b55cb --- /dev/null +++ b/web/src/parts/Selection.stories.tsx @@ -0,0 +1,144 @@ +import type { ReactNode } from 'react'; + +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import Selection from '@parts/Selection'; +import { + getProcessedStoryFrame, + storyApiHandlers, + storyBlockRoots, + storyFrameIds, +} from '@app/stories/fixtures'; +import { SelectionSeeder, defaultBYOData, withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Parts/Selection', + component: Selection, + parameters: { + layout: 'fullscreen', + msw: { + handlers: storyApiHandlers(), + }, + tanstack: { + router: { + path: '/', + }, + }, + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +function SelectionState({ children }: { children?: ReactNode }) { + return ( +
+ + {children} +
+ ); +} + +export const Closed: Story = { + render: () => , +}; + +export const SnapshotPanel: Story = { + render: () => ( + + + + ), +}; + +export const AggregatedSnapshotsPanel: Story = { + render: () => ( + + + + ), +}; + +export const BlockPanel: Story = { + render: () => ( + + + + ), +}; + +export const AggregatedBlockPanel: Story = { + render: () => ( + + + + ), +}; + +export const BYOBlockPanel: Story = { + parameters: { + tanstack: { + router: { + path: '/byo', + }, + }, + }, + decorators: [ + withForkyProviders({ + focus: { + byo: true, + byoData: defaultBYOData, + }, + }), + ], + render: () => ( + + + + ), +}; + +export const BYOBlockMissingBlockPanel: Story = { + parameters: { + tanstack: { + router: { + path: '/byo', + }, + }, + }, + decorators: [ + withForkyProviders({ + focus: { + byo: true, + byoData: getProcessedStoryFrame(storyFrameIds[0]), + }, + }), + ], + render: () => ( + + + + ), +}; diff --git a/web/src/parts/Selection.tsx b/web/src/parts/Selection.tsx index 09d563f..cbdfbb0 100644 --- a/web/src/parts/Selection.tsx +++ b/web/src/parts/Selection.tsx @@ -1,9 +1,9 @@ -import { Fragment, useEffect } from 'react'; +import { useEffect } from 'react'; -import { Dialog, Transition, TransitionChild, DialogPanel, DialogTitle } from '@headlessui/react'; +import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import classNames from 'clsx'; -import { useLocation } from 'wouter'; +import { usePathname } from '@hooks/useAppNavigation'; import AggregatedBlockSummary from '@components/AggregatedBlockSummary'; import AggregatedFramesSummary from '@components/AggregatedFramesSummary'; @@ -15,89 +15,69 @@ import useSelection from '@contexts/selection'; export default function Selection() { const { frameId, aggregatedFrameIds, frameBlock, aggregatedFramesBlock, clearAll } = useSelection(); - const [location] = useLocation(); + const location = usePathname(); const isBYO = location.startsWith('/byo'); useEffect(clearAll, [location, clearAll]); return ( -
+
- - - -
- -
-
-
- - +
+
+
+ +
+
+ + {frameId && 'Snapshot'} + {aggregatedFrameIds && 'Aggregated Snapshots'} + {frameBlock && 'Block'} + {aggregatedFramesBlock && 'Aggregated Block'} + + +
+
+ {frameId && } + {aggregatedFrameIds && } + {frameBlock && !isBYO && } + {frameBlock && isBYO && } + {aggregatedFramesBlock && ( + )} - > -
-
-
- - {frameId && 'Snapshot'} - {aggregatedFrameIds && 'Aggregated Snapshots'} - {frameBlock && 'Block'} - {aggregatedFramesBlock && 'Aggregated Block'} - -
- -
-
-
- {frameId && } - {aggregatedFrameIds && } - {frameBlock && !isBYO && } - {frameBlock && isBYO && } - {aggregatedFramesBlock && ( - - )} -
- - -
+
+
+
-
-
+
+
); diff --git a/web/src/parts/Stage.stories.tsx b/web/src/parts/Stage.stories.tsx new file mode 100644 index 0000000..65c6004 --- /dev/null +++ b/web/src/parts/Stage.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from '@storybook/tanstack-react'; + +import Stage from '@parts/Stage'; +import { getProcessedStoryFrame, storyApiHandlers, storyFrameIds } from '@app/stories/fixtures'; +import { withForkyProviders } from '@app/stories/storybook'; + +const meta = { + title: 'Parts/Stage', + component: Stage, + parameters: { + layout: 'fullscreen', + msw: { + handlers: storyApiHandlers(), + }, + tanstack: { + router: { + path: '/', + }, + }, + }, + decorators: [withForkyProviders()], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const AggregatedActiveFrames: Story = { + render: () => ( +
+ +
+ ), +}; + +export const SnapshotFrame: Story = { + decorators: [ + withForkyProviders({ + focus: { + frameId: storyFrameIds[0], + }, + }), + ], + render: () => ( +
+ +
+ ), +}; + +export const BYOEmpty: Story = { + parameters: { + tanstack: { + router: { + path: '/byo', + }, + }, + }, + decorators: [ + withForkyProviders({ + focus: { + byo: true, + }, + }), + ], + render: () => ( +
+ +
+ ), +}; + +export const BYOLoaded: Story = { + parameters: { + tanstack: { + router: { + path: '/byo', + }, + }, + }, + decorators: [ + withForkyProviders({ + focus: { + byo: true, + byoData: getProcessedStoryFrame(storyFrameIds[0]), + }, + }), + ], + render: () => ( +
+ +
+ ), +}; diff --git a/web/src/parts/Stage.tsx b/web/src/parts/Stage.tsx index 7a8d89c..a45981f 100644 --- a/web/src/parts/Stage.tsx +++ b/web/src/parts/Stage.tsx @@ -1,5 +1,7 @@ import { useEffect } from 'react'; +import classNames from 'clsx'; + import { ProcessedData } from '@app/types/graph'; import { partitionFramesBySlotLag } from '@app/utils/graph'; import useFocus from '@contexts/focus'; @@ -9,7 +11,7 @@ import Graph from '@parts/Graph'; export default function Stage() { const { ids } = useActive(); - const { byo, stop, byoData, node: focusedNode } = useFocus(); + const { byo, stop, byoData, frameId, node: focusedNode } = useFocus(); const results = useFrameQueries(ids, !byo && ids.length > 0); useEffect(() => { @@ -48,10 +50,16 @@ export default function Stage() { : partitionFramesBySlotLag(data.frames); const liveIds = live.map(frame => frame.frame.metadata.id); + // The timeline footer grows to 138px when the epoch dial appears at xl; + // the BYO/snapshot footers stay at 97px on all breakpoints. + const hasTimelineFooter = !byo && !frameId; + return (
{!isLoading && ( diff --git a/web/src/parts/Timeline.tsx b/web/src/parts/Timeline.tsx index bd6a917..9f0156b 100644 --- a/web/src/parts/Timeline.tsx +++ b/web/src/parts/Timeline.tsx @@ -1,26 +1,25 @@ -import { MinusIcon } from '@heroicons/react/20/solid'; - import Control from '@components/Control'; import EpochDial from '@components/EpochDial'; import SlotDial from '@components/SlotDial'; export default function Timeline() { return ( -
+
-
-
+
+
-
- - - - - - -
+ {/* Playhead: marks "now" across the dial stack */} + ); diff --git a/web/src/providers/application.tsx b/web/src/providers/application.tsx index ae053ae..9110d31 100644 --- a/web/src/providers/application.tsx +++ b/web/src/providers/application.tsx @@ -1,7 +1,5 @@ import { ReactNode } from 'react'; -import { navigate } from 'wouter/use-browser-location'; - import EthereumProvider, { Props as EthereumProps } from '@providers/ethereum'; import FocusProvider, { Props as FocusProps } from '@providers/focus'; import SelectionProvider, { Props as SelectionProps } from '@providers/selection'; @@ -14,10 +12,6 @@ interface Props { } function Provider({ children, ethereum, focus, selection }: Props) { - // clear the time param if it exists - if (new URLSearchParams(window.location.search).get('t')) { - navigate(window.location.pathname, { replace: true }); - } return ( diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts new file mode 100644 index 0000000..934ab91 --- /dev/null +++ b/web/src/routeTree.gen.ts @@ -0,0 +1,189 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as EventsRouteImport } from './routes/events' +import { Route as ByoRouteImport } from './routes/byo' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SnapshotFrameIdRouteImport } from './routes/snapshot.$frameId' +import { Route as NodeSplatRouteImport } from './routes/node.$' +import { Route as ByoEventsRouteImport } from './routes/byo_.events' +import { Route as SnapshotFrameIdEventsRouteImport } from './routes/snapshot_.$frameId.events' + +const EventsRoute = EventsRouteImport.update({ + id: '/events', + path: '/events', + getParentRoute: () => rootRouteImport, +} as any) +const ByoRoute = ByoRouteImport.update({ + id: '/byo', + path: '/byo', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SnapshotFrameIdRoute = SnapshotFrameIdRouteImport.update({ + id: '/snapshot/$frameId', + path: '/snapshot/$frameId', + getParentRoute: () => rootRouteImport, +} as any) +const NodeSplatRoute = NodeSplatRouteImport.update({ + id: '/node/$', + path: '/node/$', + getParentRoute: () => rootRouteImport, +} as any) +const ByoEventsRoute = ByoEventsRouteImport.update({ + id: '/byo_/events', + path: '/byo/events', + getParentRoute: () => rootRouteImport, +} as any) +const SnapshotFrameIdEventsRoute = SnapshotFrameIdEventsRouteImport.update({ + id: '/snapshot_/$frameId/events', + path: '/snapshot/$frameId/events', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/byo': typeof ByoRoute + '/events': typeof EventsRoute + '/byo/events': typeof ByoEventsRoute + '/node/$': typeof NodeSplatRoute + '/snapshot/$frameId': typeof SnapshotFrameIdRoute + '/snapshot/$frameId/events': typeof SnapshotFrameIdEventsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/byo': typeof ByoRoute + '/events': typeof EventsRoute + '/byo/events': typeof ByoEventsRoute + '/node/$': typeof NodeSplatRoute + '/snapshot/$frameId': typeof SnapshotFrameIdRoute + '/snapshot/$frameId/events': typeof SnapshotFrameIdEventsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/byo': typeof ByoRoute + '/events': typeof EventsRoute + '/byo_/events': typeof ByoEventsRoute + '/node/$': typeof NodeSplatRoute + '/snapshot/$frameId': typeof SnapshotFrameIdRoute + '/snapshot_/$frameId/events': typeof SnapshotFrameIdEventsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/byo' + | '/events' + | '/byo/events' + | '/node/$' + | '/snapshot/$frameId' + | '/snapshot/$frameId/events' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/byo' + | '/events' + | '/byo/events' + | '/node/$' + | '/snapshot/$frameId' + | '/snapshot/$frameId/events' + id: + | '__root__' + | '/' + | '/byo' + | '/events' + | '/byo_/events' + | '/node/$' + | '/snapshot/$frameId' + | '/snapshot_/$frameId/events' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ByoRoute: typeof ByoRoute + EventsRoute: typeof EventsRoute + ByoEventsRoute: typeof ByoEventsRoute + NodeSplatRoute: typeof NodeSplatRoute + SnapshotFrameIdRoute: typeof SnapshotFrameIdRoute + SnapshotFrameIdEventsRoute: typeof SnapshotFrameIdEventsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/events': { + id: '/events' + path: '/events' + fullPath: '/events' + preLoaderRoute: typeof EventsRouteImport + parentRoute: typeof rootRouteImport + } + '/byo': { + id: '/byo' + path: '/byo' + fullPath: '/byo' + preLoaderRoute: typeof ByoRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/snapshot/$frameId': { + id: '/snapshot/$frameId' + path: '/snapshot/$frameId' + fullPath: '/snapshot/$frameId' + preLoaderRoute: typeof SnapshotFrameIdRouteImport + parentRoute: typeof rootRouteImport + } + '/node/$': { + id: '/node/$' + path: '/node/$' + fullPath: '/node/$' + preLoaderRoute: typeof NodeSplatRouteImport + parentRoute: typeof rootRouteImport + } + '/byo_/events': { + id: '/byo_/events' + path: '/byo/events' + fullPath: '/byo/events' + preLoaderRoute: typeof ByoEventsRouteImport + parentRoute: typeof rootRouteImport + } + '/snapshot_/$frameId/events': { + id: '/snapshot_/$frameId/events' + path: '/snapshot/$frameId/events' + fullPath: '/snapshot/$frameId/events' + preLoaderRoute: typeof SnapshotFrameIdEventsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ByoRoute: ByoRoute, + EventsRoute: EventsRoute, + ByoEventsRoute: ByoEventsRoute, + NodeSplatRoute: NodeSplatRoute, + SnapshotFrameIdRoute: SnapshotFrameIdRoute, + SnapshotFrameIdEventsRoute: SnapshotFrameIdEventsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx new file mode 100644 index 0000000..1d2700a --- /dev/null +++ b/web/src/routes/__root.tsx @@ -0,0 +1,44 @@ +import { createRootRoute } from '@tanstack/react-router'; + +import App from '@app/App'; +import NotFound from '@app/NotFound'; +import { usePathname } from '@hooks/useAppNavigation'; +import { parseAppRoute } from '@utils/routes'; + +/* + * The root route renders the single, persistent instance for every + * view; the view config (node, snapshot, BYO, events overlay) is derived + * from the location. Keeping one App alive across navigation is what + * preserves player state — the focused time and play/pause — when moving + * between the aggregated, node and snapshot views (the focus provider + * reconciles route-driven props in place). The leaf files in src/routes/ + * only define the matchable, typed route tree; they render nothing. + */ +export const Route = createRootRoute({ + component: function Root() { + const pathname = usePathname(); + const route = parseAppRoute(pathname); + + switch (route?.kind) { + case 'home': + return ; + case 'events': + return ; + case 'byo': + return ; + case 'byoEvents': + return ; + case 'node': + return ; + case 'nodeEvents': + return ; + case 'snapshot': + return ; + case 'snapshotEvents': + return ; + default: + return ; + } + }, + notFoundComponent: NotFound, +}); diff --git a/web/src/routes/byo.tsx b/web/src/routes/byo.tsx new file mode 100644 index 0000000..582f528 --- /dev/null +++ b/web/src/routes/byo.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/byo')({}); diff --git a/web/src/routes/byo_.events.tsx b/web/src/routes/byo_.events.tsx new file mode 100644 index 0000000..b32c81f --- /dev/null +++ b/web/src/routes/byo_.events.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/byo_/events')({}); diff --git a/web/src/routes/events.tsx b/web/src/routes/events.tsx new file mode 100644 index 0000000..f13660f --- /dev/null +++ b/web/src/routes/events.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/events')({}); diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx new file mode 100644 index 0000000..42da085 --- /dev/null +++ b/web/src/routes/index.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/')({}); diff --git a/web/src/routes/node.$.tsx b/web/src/routes/node.$.tsx new file mode 100644 index 0000000..65c8eaf --- /dev/null +++ b/web/src/routes/node.$.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/node/$')({}); diff --git a/web/src/routes/snapshot.$frameId.tsx b/web/src/routes/snapshot.$frameId.tsx new file mode 100644 index 0000000..4182a02 --- /dev/null +++ b/web/src/routes/snapshot.$frameId.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/snapshot/$frameId')({}); diff --git a/web/src/routes/snapshot_.$frameId.events.tsx b/web/src/routes/snapshot_.$frameId.events.tsx new file mode 100644 index 0000000..cdac24a --- /dev/null +++ b/web/src/routes/snapshot_.$frameId.events.tsx @@ -0,0 +1,4 @@ +import { createFileRoute } from '@tanstack/react-router'; + +/* Matcher only — the persistent is rendered by the root route. */ +export const Route = createFileRoute('/snapshot_/$frameId/events')({}); diff --git a/web/src/stories/fixtures.ts b/web/src/stories/fixtures.ts new file mode 100644 index 0000000..09cbb2e --- /dev/null +++ b/web/src/stories/fixtures.ts @@ -0,0 +1,582 @@ +import { http, HttpResponse, delay } from 'msw'; + +import type { + EthereumNow, + Frame, + FrameFilter, + FrameMetadata, + ForkChoice, + ForkChoiceNode, + GetEthereumNowResponse, + GetEthereumSpecResponse, + GetFrameResponse, + ListMetadataNodesResponse, + ListMetadataResponse, + MetadataQuery, +} from '@api'; +import type { ProcessedData } from '@app/types/graph'; +import { processForkChoiceData } from '@utils/graph'; + +export const storyNetworkName = 'hoodi-story'; + +export const storySpec = { + seconds_per_slot: 12, + slots_per_epoch: 32, + genesis_time: '2024-01-01T00:00:00.000Z', +}; + +export const storyNow: EthereumNow = { + slot: 113, + epoch: 3, +}; + +const baseGenesis = new Date(storySpec.genesis_time).getTime(); +const storyNodes = ['ams3-teku-001', 'syd1-lighthouse-001', 'sfo2-prysm-001']; + +function root(value: number): string { + return `0x${value.toString(16).padStart(64, '0')}`; +} + +function atSlot(slot: number, offsetMs = 0): string { + return new Date(baseGenesis + slot * storySpec.seconds_per_slot * 1000 + offsetMs).toISOString(); +} + +function forkChoiceNode({ + slot, + blockRoot, + parentRoot, + weight, + validity = 'valid', + graffiti, +}: { + slot: number; + blockRoot: string; + parentRoot: string; + weight: number; + validity?: ForkChoiceNode['validity']; + graffiti?: string; +}): ForkChoiceNode { + return { + slot: slot.toString(), + block_root: blockRoot, + parent_root: parentRoot, + justified_epoch: '3', + finalized_epoch: '3', + weight: weight.toString(), + validity, + execution_block_hash: root(10_000 + slot + weight), + extra_data: { + graffiti: graffiti ?? `story-slot-${slot}`, + proposer_boost_root: blockRoot, + unrealized_justified_root: root(103), + unrealized_finalized_root: root(100), + }, + }; +} + +const roots = { + genesis: root(99), + finalized: root(100), + slot101: root(101), + slot102: root(102), + justified: root(103), + slot104: root(104), + slot105: root(105), + slot106: root(106), + slot107: root(107), + fork105: root(205), + fork106: root(206), + fork107: root(207), + orphan106: root(306), + missingParent: root(50), +}; + +export const storyBlockRoots = roots; + +function forkChoiceAlpha(): ForkChoice { + return { + finalized_checkpoint: { epoch: '3', root: roots.finalized }, + justified_checkpoint: { epoch: '3', root: roots.justified }, + fork_choice_nodes: [ + forkChoiceNode({ + slot: 100, + blockRoot: roots.finalized, + parentRoot: roots.genesis, + weight: 1_000, + graffiti: 'finalized checkpoint', + }), + forkChoiceNode({ + slot: 101, + blockRoot: roots.slot101, + parentRoot: roots.finalized, + weight: 990, + }), + forkChoiceNode({ + slot: 102, + blockRoot: roots.slot102, + parentRoot: roots.slot101, + weight: 980, + }), + forkChoiceNode({ + slot: 103, + blockRoot: roots.justified, + parentRoot: roots.slot102, + weight: 970, + validity: 'optimistic', + graffiti: 'justified optimistic checkpoint', + }), + forkChoiceNode({ + slot: 104, + blockRoot: roots.slot104, + parentRoot: roots.justified, + weight: 960, + }), + forkChoiceNode({ + slot: 105, + blockRoot: roots.slot105, + parentRoot: roots.slot104, + weight: 950, + }), + forkChoiceNode({ + slot: 106, + blockRoot: roots.slot106, + parentRoot: roots.slot105, + weight: 930, + }), + forkChoiceNode({ + slot: 107, + blockRoot: roots.slot107, + parentRoot: roots.slot106, + weight: 910, + }), + forkChoiceNode({ + slot: 108, + blockRoot: roots.fork105, + parentRoot: roots.slot105, + weight: 430, + }), + forkChoiceNode({ + slot: 109, + blockRoot: roots.fork106, + parentRoot: roots.fork105, + weight: 350, + validity: 'invalid', + graffiti: 'invalid fork branch', + }), + ], + }; +} + +function forkChoiceBeta(): ForkChoice { + return { + finalized_checkpoint: { epoch: '3', root: roots.finalized }, + justified_checkpoint: { epoch: '3', root: roots.justified }, + fork_choice_nodes: [ + forkChoiceNode({ + slot: 100, + blockRoot: roots.finalized, + parentRoot: roots.genesis, + weight: 1_050, + }), + forkChoiceNode({ + slot: 101, + blockRoot: roots.slot101, + parentRoot: roots.finalized, + weight: 1_030, + }), + forkChoiceNode({ + slot: 102, + blockRoot: roots.slot102, + parentRoot: roots.slot101, + weight: 1_010, + }), + forkChoiceNode({ + slot: 103, + blockRoot: roots.justified, + parentRoot: roots.slot102, + weight: 1_000, + }), + forkChoiceNode({ + slot: 104, + blockRoot: roots.slot104, + parentRoot: roots.justified, + weight: 990, + }), + forkChoiceNode({ + slot: 105, + blockRoot: roots.slot105, + parentRoot: roots.slot104, + weight: 520, + }), + forkChoiceNode({ + slot: 106, + blockRoot: roots.slot106, + parentRoot: roots.slot105, + weight: 510, + graffiti: 'abandoned head before reorg', + }), + forkChoiceNode({ + slot: 107, + blockRoot: roots.slot107, + parentRoot: roots.slot106, + weight: 500, + }), + forkChoiceNode({ + slot: 108, + blockRoot: roots.fork105, + parentRoot: roots.slot105, + weight: 980, + graffiti: 'heavier fork after reorg', + }), + forkChoiceNode({ + slot: 109, + blockRoot: roots.fork106, + parentRoot: roots.fork105, + weight: 960, + }), + forkChoiceNode({ + slot: 110, + blockRoot: roots.fork107, + parentRoot: roots.fork106, + weight: 940, + }), + ], + }; +} + +function forkChoiceGamma(): ForkChoice { + return { + finalized_checkpoint: { epoch: '3', root: roots.finalized }, + justified_checkpoint: { epoch: '3', root: roots.justified }, + fork_choice_nodes: [ + forkChoiceNode({ + slot: 100, + blockRoot: roots.finalized, + parentRoot: roots.genesis, + weight: 990, + }), + forkChoiceNode({ + slot: 101, + blockRoot: roots.slot101, + parentRoot: roots.finalized, + weight: 980, + }), + forkChoiceNode({ + slot: 102, + blockRoot: roots.slot102, + parentRoot: roots.slot101, + weight: 970, + }), + forkChoiceNode({ + slot: 103, + blockRoot: roots.justified, + parentRoot: roots.slot102, + weight: 960, + }), + forkChoiceNode({ + slot: 104, + blockRoot: roots.slot104, + parentRoot: roots.justified, + weight: 950, + }), + forkChoiceNode({ + slot: 105, + blockRoot: roots.slot105, + parentRoot: roots.slot104, + weight: 930, + }), + forkChoiceNode({ + slot: 106, + blockRoot: roots.slot106, + parentRoot: roots.slot105, + weight: 910, + validity: 'optimistic', + }), + forkChoiceNode({ + slot: 107, + blockRoot: roots.slot107, + parentRoot: roots.slot106, + weight: 890, + }), + forkChoiceNode({ + slot: 108, + blockRoot: roots.fork105, + parentRoot: roots.slot105, + weight: 410, + }), + ], + }; +} + +function frameMetadata({ + id, + node, + fetchedAt, + labels = [], + consensusClient = 'teku', + eventSource = 'beacon_node', +}: { + id: string; + node: string; + fetchedAt: string; + labels?: string[]; + consensusClient?: string; + eventSource?: string; +}): FrameMetadata { + const slot = Math.floor( + (new Date(fetchedAt).getTime() - baseGenesis) / 1000 / storySpec.seconds_per_slot, + ); + + return { + id, + node, + fetched_at: fetchedAt, + wall_clock_slot: slot, + wall_clock_epoch: Math.floor(slot / storySpec.slots_per_epoch), + labels, + consensus_client: consensusClient, + event_source: eventSource, + }; +} + +function frame(metadata: FrameMetadata, data: ForkChoice): Required { + return { metadata, data }; +} + +export const storyFrames = { + alpha: frame( + frameMetadata({ + id: 'frame-alpha', + node: storyNodes[0], + fetchedAt: atSlot(110, 1_000), + labels: ['region=ams3', 'consensus_client_version=v24.12.0'], + consensusClient: 'teku', + }), + forkChoiceAlpha(), + ), + beta: frame( + frameMetadata({ + id: 'frame-beta', + node: storyNodes[1], + fetchedAt: atSlot(110, 4_000), + labels: ['region=syd1', 'consensus_client_version=v5.3.0'], + consensusClient: 'lighthouse', + }), + forkChoiceBeta(), + ), + gamma: frame( + frameMetadata({ + id: 'frame-gamma', + node: storyNodes[2], + fetchedAt: atSlot(110, 7_000), + labels: ['region=sfo2', 'consensus_client_version=v5.1.2'], + consensusClient: 'prysm', + }), + forkChoiceGamma(), + ), +}; + +const eventBeforeMetadata = frameMetadata({ + id: 'event-before-reorg-1', + node: storyNodes[0], + fetchedAt: atSlot(106, 3_000), + consensusClient: 'teku', + eventSource: 'xatu_reorg_event', + labels: [ + 'xatu_event_id=reorg-1', + 'xatu_reorg_frame_timing=before', + 'xatu_reorg_event_slot=106', + 'xatu_reorg_event_epoch=3', + `xatu_reorg_event_old_head_block=${roots.slot107}`, + `xatu_reorg_event_new_head_block=${roots.fork107}`, + 'xatu_reorg_event_depth=3', + 'consensus_client_implementation=teku', + ], +}); + +const eventAfterMetadata = frameMetadata({ + id: 'event-after-reorg-1', + node: storyNodes[0], + fetchedAt: atSlot(106, 6_000), + consensusClient: 'teku', + eventSource: 'xatu_reorg_event', + labels: [ + 'xatu_event_id=reorg-1', + 'xatu_reorg_frame_timing=after', + 'xatu_reorg_event_slot=106', + 'xatu_reorg_event_epoch=3', + `xatu_reorg_event_old_head_block=${roots.slot107}`, + `xatu_reorg_event_new_head_block=${roots.fork107}`, + 'xatu_reorg_event_depth=3', + 'consensus_client_implementation=teku', + ], +}); + +const singleEventMetadata = frameMetadata({ + id: 'event-single-invalid', + node: storyNodes[2], + fetchedAt: atSlot(104, 4_000), + consensusClient: 'prysm', + eventSource: 'xatu_reorg_event', + labels: [ + 'xatu_event_id=invalid-1', + 'xatu_reorg_event_slot=104', + 'xatu_reorg_event_epoch=3', + 'xatu_reorg_event_depth=1', + 'consensus_client_implementation=prysm', + ], +}); + +const storyEventFrames = [ + frame(eventBeforeMetadata, forkChoiceAlpha()), + frame(eventAfterMetadata, forkChoiceBeta()), + frame(singleEventMetadata, forkChoiceGamma()), +]; + +export const storyFrameList: Required[] = Object.values(storyFrames); +export const storyAllFrames: Required[] = [...storyFrameList, ...storyEventFrames]; +export const storyFrameIds = storyFrameList.map(({ metadata }) => metadata.id); +export const storyEventMetadata = storyEventFrames.map(({ metadata }) => metadata); +export const storyAllMetadata = storyAllFrames.map(({ metadata }) => metadata); + +export function cloneFrame(frameToClone: Required): Required { + return JSON.parse(JSON.stringify(frameToClone)) as Required; +} + +export function getStoryFrame(id: string): Required | undefined { + const found = storyAllFrames.find(({ metadata }) => metadata.id === id); + return found ? cloneFrame(found) : undefined; +} + +export function getProcessedStoryFrame(id: string): ProcessedData { + const found = getStoryFrame(id); + if (!found) { + throw new Error(`Unknown story frame: ${id}`); + } + return processForkChoiceData(found); +} + +export function getProcessedStoryFrames(ids = storyFrameIds): ProcessedData[] { + return ids.map(getProcessedStoryFrame); +} + +function filterMetadata(metadata: FrameMetadata[], filter?: FrameFilter): FrameMetadata[] { + if (!filter) return metadata; + + return metadata.filter(item => { + if (filter.node && item.node !== filter.node) return false; + if (filter.slot !== undefined && item.wall_clock_slot !== filter.slot) return false; + if (filter.epoch !== undefined && item.wall_clock_epoch !== filter.epoch) return false; + if (filter.consensus_client && item.consensus_client !== filter.consensus_client) return false; + if (filter.event_source && item.event_source !== filter.event_source) return false; + if (filter.after && new Date(item.fetched_at).getTime() < new Date(filter.after).getTime()) { + return false; + } + if (filter.before && new Date(item.fetched_at).getTime() > new Date(filter.before).getTime()) { + return false; + } + if (filter.labels?.length && !filter.labels.every(label => item.labels.includes(label))) { + return false; + } + return true; + }); +} + +function paginateMetadata(metadata: FrameMetadata[], pagination?: MetadataQuery['pagination']) { + const offset = pagination?.offset ?? 0; + const limit = pagination?.limit ?? metadata.length; + return metadata.slice(offset, offset + limit); +} + +export function storyApiHandlers({ + metadata = storyAllMetadata, + frames = storyAllFrames, + metadataDelay = 0, + frameDelay = 0, + specDelay = 0, + nowDelay = 0, + specError = false, + nowError = false, + metadataError = false, + frameError = false, +}: { + metadata?: FrameMetadata[]; + frames?: Required[]; + metadataDelay?: number; + frameDelay?: number; + specDelay?: number; + nowDelay?: number; + specError?: boolean; + nowError?: boolean; + metadataError?: boolean; + frameError?: boolean; +} = {}) { + return [ + http.get('*/api/v1/ethereum/spec', async () => { + if (specDelay) await delay(specDelay); + if (specError) { + return HttpResponse.json({ code: 500, message: 'Story spec failure' }, { status: 500 }); + } + return HttpResponse.json({ + data: { network_name: storyNetworkName, spec: storySpec }, + } satisfies GetEthereumSpecResponse); + }), + http.get('*/api/v1/ethereum/now', async () => { + if (nowDelay) await delay(nowDelay); + if (nowError) { + return HttpResponse.json({ code: 500, message: 'Story now failure' }, { status: 500 }); + } + return HttpResponse.json({ data: storyNow } satisfies GetEthereumNowResponse); + }), + http.post('*/api/v1/metadata/nodes', async ({ request }) => { + if (metadataError) { + return HttpResponse.json({ code: 500, message: 'Story nodes failure' }, { status: 500 }); + } + if (metadataDelay) await delay(metadataDelay); + const body = (await request.json().catch(() => ({}))) as MetadataQuery; + const filtered = filterMetadata(metadata, body.filter); + const nodes = [...new Set(filtered.map(({ node }) => node))].sort(); + return HttpResponse.json({ + data: { + nodes, + pagination: { total: nodes.length }, + }, + } satisfies ListMetadataNodesResponse); + }), + http.post('*/api/v1/metadata', async ({ request }) => { + if (metadataError) { + return HttpResponse.json({ code: 500, message: 'Story metadata failure' }, { status: 500 }); + } + if (metadataDelay) await delay(metadataDelay); + const body = (await request.json().catch(() => ({}))) as MetadataQuery; + const filtered = filterMetadata(metadata, body.filter).sort( + (a, b) => new Date(a.fetched_at).getTime() - new Date(b.fetched_at).getTime(), + ); + const paginated = paginateMetadata(filtered, body.pagination); + + return HttpResponse.json({ + data: { + frames: paginated, + pagination: { total: filtered.length }, + }, + } satisfies ListMetadataResponse); + }), + http.get('*/api/v1/frames/:id', async ({ params }) => { + if (frameError) { + return HttpResponse.json({ code: 500, message: 'Story frame failure' }, { status: 500 }); + } + if (frameDelay) await delay(frameDelay); + const id = Array.isArray(params.id) ? params.id[0] : params.id; + const found = frames.find(({ metadata: { id: frameId } }) => frameId === id); + if (!found) { + return HttpResponse.json({ code: 404, message: 'Frame not found' }, { status: 404 }); + } + + return HttpResponse.json({ + data: { + frame: cloneFrame(found), + }, + } satisfies GetFrameResponse); + }), + ]; +} diff --git a/web/src/stories/storybook.tsx b/web/src/stories/storybook.tsx new file mode 100644 index 0000000..b3180de --- /dev/null +++ b/web/src/stories/storybook.tsx @@ -0,0 +1,138 @@ +/* eslint-disable react-refresh/only-export-components */ +import { ReactNode, useEffect } from 'react'; + +import type { Decorator } from '@storybook/tanstack-react'; +import classNames from 'clsx'; + +import type { ProcessedData } from '@app/types/graph'; +import useFocus from '@contexts/focus'; +import useSelection, { AggregatedFramesBlock, FrameBlock } from '@contexts/selection'; +import ApplicationProvider from '@providers/application'; +import { + getProcessedStoryFrame, + storyFrameIds, + storyNetworkName, + storyNow, + storySpec, +} from '@app/stories/fixtures'; + +const storyGenesisTime = new Date(storySpec.genesis_time).getTime(); + +export const defaultEthereumProps = { + genesisTime: storyGenesisTime, + secondsPerSlot: storySpec.seconds_per_slot, + slotsPerEpoch: storySpec.slots_per_epoch, + networkName: storyNetworkName, +}; + +export const defaultFocusProps = { + initialTime: storyGenesisTime + (storyNow.slot - 2) * storySpec.seconds_per_slot * 1000, + playing: false, + byo: false, +}; + +export function StorySurface({ + children, + className, + padded = true, +}: { + children: ReactNode; + className?: string; + padded?: boolean; +}) { + return ( +
+ {children} +
+ ); +} + +export function PanelSurface({ children, className }: { children: ReactNode; className?: string }) { + return ( + +
{children}
+
+ ); +} + +export function withForkyProviders({ + focus, +}: { + focus?: Partial & { + node?: string; + frameId?: string; + byoData?: ProcessedData; + }; +} = {}): Decorator { + return Story => { + const { byoData, ...focusProps } = focus ?? {}; + return ( + + {byoData && } + + + ); + }; +} + +export function BYODataSeeder({ data }: { data: ProcessedData }) { + const { setBYOData } = useFocus(); + + useEffect(() => { + setBYOData(data); + return () => setBYOData(undefined); + }, [data, setBYOData]); + + return null; +} + +export function SelectionSeeder({ + frameId, + aggregatedFrameIds, + frameBlock, + aggregatedFramesBlock, +}: { + frameId?: string; + aggregatedFrameIds?: string[]; + frameBlock?: FrameBlock; + aggregatedFramesBlock?: AggregatedFramesBlock; +}) { + const { clearAll, setAggregatedFrameIds, setAggregatedFramesBlock, setFrameBlock, setFrameId } = + useSelection(); + + useEffect(() => { + if (frameId) setFrameId(frameId); + else if (aggregatedFrameIds) setAggregatedFrameIds(aggregatedFrameIds); + else if (frameBlock) setFrameBlock(frameBlock); + else if (aggregatedFramesBlock) setAggregatedFramesBlock(aggregatedFramesBlock); + else clearAll(); + + return clearAll; + }, [ + aggregatedFrameIds, + aggregatedFramesBlock, + clearAll, + frameBlock, + frameId, + setAggregatedFrameIds, + setAggregatedFramesBlock, + setFrameBlock, + setFrameId, + ]); + + return null; +} + +export const defaultBYOData = getProcessedStoryFrame(storyFrameIds[0]); diff --git a/web/src/styles/global.css b/web/src/styles/global.css deleted file mode 100644 index 150ffc3..0000000 --- a/web/src/styles/global.css +++ /dev/null @@ -1,13 +0,0 @@ -@import "tailwindcss"; - -@custom-variant dark (&:where(.dark, .dark *)); - -/* fix for safari https://github.com/mdn/browser-compat-data/issues/17726 */ -.drop-shadow-sm, -.drop-shadow, -.drop-shadow-md, -.drop-shadow-lg, -.drop-shadow-xl, -.drop-shadow-2xl { - transform: translateZ(0) -} \ No newline at end of file diff --git a/web/src/types/api.ts b/web/src/types/api.ts index d837341..271a117 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -1,28 +1,9 @@ -export interface Checkpoint { - epoch: string; - root: string; -} - -export interface ForkChoiceNode { - slot: string; - block_root: string; - parent_root: string; - justified_epoch: string; - finalized_epoch: string; - weight: string; - validity: string; - execution_block_hash: string; - extra_data?: Record; -} +/** + * App-specific API view types. The wire types live in the generated client + * (`@api`) — only types that intentionally differ from the spec belong here. + */ -export interface ForkChoiceData { - justified_checkpoint?: Checkpoint; - finalized_checkpoint?: Checkpoint; - fork_choice_nodes?: ForkChoiceNode[]; - extra_data?: unknown; -} - -/* REQUESTS */ +/** Frame filter with the event sources the UI knows how to present. */ export interface FrameFilter { node?: string; before?: string; @@ -33,104 +14,3 @@ export interface FrameFilter { consensus_client?: string; event_source?: 'unknown' | 'beacon_node' | 'xatu_polling' | 'xatu_reorg_event'; } - -export interface PaginationCursor { - limit?: number; - offset?: number; - order?: string; -} - -export interface V1MetadataListSlotsRequest { - filter?: FrameFilter; - pagination?: PaginationCursor; -} - -export interface V1MetadataListRequest { - filter?: FrameFilter; - pagination?: PaginationCursor; -} - -export interface V1MetadataListNodesRequest { - filter?: FrameFilter; - pagination?: PaginationCursor; -} - -export interface V1MetadataListEpochsRequest { - filter?: FrameFilter; - pagination?: PaginationCursor; -} - -export interface V1MetadataListLabelsRequest { - filter?: FrameFilter; - pagination?: PaginationCursor; -} - -/* RESPONSE */ -export interface Response { - data?: T; -} - -export interface Frame { - data?: ForkChoiceData; - metadata?: FrameMetaData; -} - -export interface FrameMetaData { - id: string; - node: string; - fetched_at: string; - wall_clock_slot: number; - wall_clock_epoch: number; - labels?: string[] | null; - consensus_client?: string | null; - event_source?: string | null; -} - -export interface EthereumSpec { - seconds_per_slot: number; - slots_per_epoch: number; - genesis_time: string; -} - -export interface PaginationResponse { - total?: number; -} - -export interface V1MetadataListResponse { - frames?: FrameMetaData[]; - pagination?: PaginationResponse; -} - -export interface V1GetEthereumSpecResponse { - network_name: string; - spec?: EthereumSpec; -} - -export interface V1GetEthereumNowResponse { - slot?: number; - epoch?: number; -} - -export interface V1MetadataListNodesResponse { - nodes?: string[]; - pagination: PaginationResponse; -} - -export interface V1MetadataListSlotsResponse { - slots?: number[]; - pagination: PaginationResponse; -} - -export interface V1MetadataListEpochsResponse { - epochs?: number[]; - pagination: PaginationResponse; -} - -export interface V1MetadataListLabelsResponse { - labels?: string[]; - pagination: PaginationResponse; -} - -export interface V1GetFrameResponse { - frame?: Frame; -} diff --git a/web/src/types/graph.ts b/web/src/types/graph.ts index ba44c10..40a63ad 100644 --- a/web/src/types/graph.ts +++ b/web/src/types/graph.ts @@ -1,6 +1,6 @@ import Graphology from 'graphology'; -import { V1GetFrameResponse, FrameMetaData, Checkpoint } from '@app/types/api'; +import { Frame, FrameMetadata, Checkpoint } from '@api'; export interface ProcessedForkChoiceNode { slot: number; @@ -56,7 +56,7 @@ export interface WeightedGraphAttributes extends GraphAttributes { export interface AggregatedGraphAttributes extends GraphAttributes { nodes: { - metadata: FrameMetaData; + metadata: FrameMetadata; head?: WeightedNodeAttributes; justifiedCheckpoint?: Checkpoint; finalizedCheckpoint?: Checkpoint; @@ -83,7 +83,7 @@ export interface AggregatedNodeAttributes extends NodeAttributes { } export type ProcessedData = { - frame: Required['frame']>; + frame: Required; graph: WeightedGraph; }; diff --git a/web/src/utils/__tests__/api.test.ts b/web/src/utils/__tests__/api.test.ts index 0d44e65..f646ae5 100644 --- a/web/src/utils/__tests__/api.test.ts +++ b/web/src/utils/__tests__/api.test.ts @@ -1,4 +1,4 @@ -import { ForkChoiceNode, ForkChoiceData } from '@app/types/api'; +import { ForkChoiceNode, ForkChoice } from '@api'; import { equalForkChoiceNode, equalForkChoiceData, getCheckpointType } from '@utils/api'; function generateForkChoiceNode(): ForkChoiceNode { @@ -22,7 +22,7 @@ function generateForkChoiceNode(): ForkChoiceNode { }; } -function generateForkChoiceData(): ForkChoiceData { +function generateForkChoiceData(): ForkChoice { return { justified_checkpoint: { epoch: '1', root: '0x1' }, finalized_checkpoint: { epoch: '2', root: '0x2' }, @@ -82,13 +82,13 @@ describe('api', () => { expect(equalForkChoiceData(undefined, undefined)).toBe(false); }); - it('should return true for equal ForkChoiceData', () => { + it('should return true for equal ForkChoice', () => { const a = generateForkChoiceData(); const b = generateForkChoiceData(); expect(equalForkChoiceData(a, b)).toBe(true); }); - it('should return true for equal ForkChoiceData and ForkChoiceNodes', () => { + it('should return true for equal ForkChoice and ForkChoiceNodes', () => { const a = generateForkChoiceData(); const b = generateForkChoiceData(); a.fork_choice_nodes = [generateForkChoiceNode()]; diff --git a/web/src/utils/__tests__/functions.test.ts b/web/src/utils/__tests__/functions.test.ts index 7ca2e8f..14f7282 100644 --- a/web/src/utils/__tests__/functions.test.ts +++ b/web/src/utils/__tests__/functions.test.ts @@ -97,9 +97,8 @@ describe('functions', () => { it('should return different bigints for multiple calls within the same range', () => { const min = BigInt(1); const max = BigInt(100); - const result1 = randomBigInt(min, max); - const result2 = randomBigInt(min, max); - expect(result1).not.toBe(result2); + const results = new Set(Array.from({ length: 20 }, () => randomBigInt(min, max))); + expect(results.size).toBeGreaterThan(1); }); it('should return a bigint within large range', () => { diff --git a/web/src/utils/__tests__/graph.test.ts b/web/src/utils/__tests__/graph.test.ts index bfe76aa..a969d86 100644 --- a/web/src/utils/__tests__/graph.test.ts +++ b/web/src/utils/__tests__/graph.test.ts @@ -1,6 +1,6 @@ import Graphology from 'graphology'; -import { Frame } from '@app/types/api'; +import { Frame } from '@api'; import { NodeAttributes, WeightedNodeAttributes, diff --git a/web/src/utils/__tests__/routes.test.ts b/web/src/utils/__tests__/routes.test.ts new file mode 100644 index 0000000..ab1f799 --- /dev/null +++ b/web/src/utils/__tests__/routes.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; + +import { eventsPathFor, parseAppRoute, parseNodeSplat } from '@utils/routes'; + +describe('routes', () => { + it.each([ + ['/', 'home'], + ['/events', 'events'], + ['/byo', 'byo'], + ['/byo/events', 'byoEvents'], + ['/node/ams3-teku-001', 'node'], + ['/node/ams3-teku-001/events', 'nodeEvents'], + ['/node/ethpandaops/mainnet/node-001', 'node'], + ['/node/ethpandaops/mainnet/node-001/events', 'nodeEvents'], + ['/snapshot/frame-alpha', 'snapshot'], + ['/snapshot/frame-alpha/events', 'snapshotEvents'], + ])('parses %s as %s', (path, kind) => { + expect(parseAppRoute(path)?.kind).toBe(kind); + }); + + it('parses multi-segment node ids with literal slashes', () => { + const route = parseAppRoute('/node/ethpandaops/mainnet/node-001'); + expect(route).toMatchObject({ + kind: 'node', + nodeId: 'ethpandaops/mainnet/node-001', + path: '/node/ethpandaops/mainnet/node-001', + }); + + const events = parseAppRoute('/node/ethpandaops/mainnet/node-001/events'); + expect(events).toMatchObject({ + kind: 'nodeEvents', + nodeId: 'ethpandaops/mainnet/node-001', + closeTo: '/node/ethpandaops/mainnet/node-001', + }); + }); + + it('canonicalizes encoded slashes in node ids to literal slashes', () => { + expect(parseAppRoute('/node/sentry%2Fone')).toMatchObject({ + kind: 'node', + nodeId: 'sentry/one', + path: '/node/sentry/one', + }); + }); + + it('encodes route params when normalizing paths', () => { + expect(parseAppRoute('/snapshot/frame%20alpha/events')?.path).toBe( + '/snapshot/frame%20alpha/events', + ); + }); + + it.each([ + [undefined, undefined], + ['', undefined], + ['ams3-teku-001', { nodeId: 'ams3-teku-001', events: false }], + ['ams3-teku-001/events', { nodeId: 'ams3-teku-001', events: true }], + ['ethpandaops/mainnet/node-001', { nodeId: 'ethpandaops/mainnet/node-001', events: false }], + [ + 'ethpandaops/mainnet/node-001/events', + { nodeId: 'ethpandaops/mainnet/node-001', events: true }, + ], + ['events', { nodeId: 'events', events: false }], + ])('parses node splat %s', (splat, expected) => { + expect(parseNodeSplat(splat)).toEqual(expected); + }); + + it.each([ + ['/', '/events'], + ['/events', '/events'], + ['/byo', '/byo/events'], + ['/byo/events', '/byo/events'], + ['/node/ams3-teku-001', '/node/ams3-teku-001/events'], + ['/node/ams3-teku-001/events', '/node/ams3-teku-001/events'], + ['/node/ethpandaops/mainnet/node-001', '/node/ethpandaops/mainnet/node-001/events'], + ['/node/ethpandaops/mainnet/node-001/events', '/node/ethpandaops/mainnet/node-001/events'], + ['/snapshot/frame-alpha', '/snapshot/frame-alpha/events'], + ['/snapshot/frame-alpha/events', '/snapshot/frame-alpha/events'], + ['/unknown', '/events'], + ])('builds event route %s -> %s', (path, eventsPath) => { + expect(eventsPathFor(path)).toBe(eventsPath); + }); +}); diff --git a/web/src/utils/api-config.ts b/web/src/utils/api-config.ts new file mode 100644 index 0000000..05590b1 --- /dev/null +++ b/web/src/utils/api-config.ts @@ -0,0 +1,22 @@ +/** + * Runtime configuration for the generated API client (hey-api). + * + * Wired in via `openapi-ts.config.ts` (`runtimeConfigPath`). The generated + * client imports `createClientConfig` from here and calls it with its default + * config. Forky's API is unauthenticated and served from the same origin, so + * the base URL defaults to a relative path. + */ + +export const BASE_URL = import.meta.env.VITEST + ? 'http://localhost:5555' + : import.meta.env.VITE_API_URL || ''; + +interface ClientConfig { + baseUrl?: string; + fetch?: typeof fetch; +} + +export const createClientConfig = (config: T): T & { baseUrl: string } => ({ + ...config, + baseUrl: BASE_URL, +}); diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 5110902..7242923 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -1,4 +1,4 @@ -import { ForkChoiceData, ForkChoiceNode } from '@app/types/api'; +import type { ForkChoice, ForkChoiceNode } from '@api'; import { randomHex, randomBigInt, randomInt } from '@utils/functions'; export function equalForkChoiceNode(a?: ForkChoiceNode, b?: ForkChoiceNode) { @@ -12,7 +12,7 @@ export function equalForkChoiceNode(a?: ForkChoiceNode, b?: ForkChoiceNode) { return true; } -export function equalForkChoiceData(a?: ForkChoiceData, b?: ForkChoiceData) { +export function equalForkChoiceData(a?: ForkChoice, b?: ForkChoice) { if (!a || !b) return false; // check if nodes length changed @@ -75,7 +75,7 @@ export function generateRandomForkChoiceData({ continueForkChance?: number; invalidChance?: number; } - | undefined = {}): ForkChoiceData { + | undefined = {}): ForkChoice { const totalSlots = randomInt(minSlots, maxSlots); const startSlot = totalSlots + @@ -133,7 +133,7 @@ export function generateRandomForkChoiceData({ justified_epoch: justifiedCheckpoint.epoch, finalized_epoch: finalizedCheckpoint.epoch, weight: currentWeight.toString(), - validity: invalidChance > Math.random() ? 'INVALID' : 'VALID', + validity: invalidChance > Math.random() ? 'invalid' : 'valid', execution_block_hash: randomHex(64), extra_data: extraData, }; @@ -152,14 +152,14 @@ export function generateRandomForkChoiceData({ const canonicalWeight = BigInt(nextForkChoiceNode.weight); const nextWeight = randomBigInt(0n, canonicalWeight); - let forkedNode = { + let forkedNode: ForkChoiceNode = { slot: (Number.parseInt(forkChoiceNode.slot) + randomInt(1, 5)).toString(), block_root: randomHex(64), parent_root: forkChoiceNode.block_root, justified_epoch: justifiedCheckpoint.epoch, finalized_epoch: finalizedCheckpoint.epoch, weight: nextWeight.toString(), - validity: invalidChance > Math.random() ? 'INVALID' : 'VALID', + validity: invalidChance > Math.random() ? 'invalid' : 'valid', execution_block_hash: randomHex(64), extra_data: { state_root: randomHex(64), @@ -184,7 +184,7 @@ export function generateRandomForkChoiceData({ justified_epoch: justifiedCheckpoint.epoch, finalized_epoch: finalizedCheckpoint.epoch, weight: randomBigInt(0n, BigInt(forkedNode.weight)).toString(), - validity: invalidChance > Math.random() ? 'INVALID' : 'VALID', + validity: invalidChance > Math.random() ? 'invalid' : 'valid', execution_block_hash: randomHex(64), extra_data: { state_root: randomHex(64), diff --git a/web/src/utils/graph.ts b/web/src/utils/graph.ts index f01cfd0..ec14608 100644 --- a/web/src/utils/graph.ts +++ b/web/src/utils/graph.ts @@ -1,7 +1,7 @@ import Graphology from 'graphology'; import { Attributes } from 'graphology-types'; -import { ForkChoiceNode, Frame } from '@app/types/api'; +import { ForkChoiceNode, Frame } from '@api'; import { Graph, WeightedGraph, diff --git a/web/src/utils/routes.ts b/web/src/utils/routes.ts new file mode 100644 index 0000000..78e7b5d --- /dev/null +++ b/web/src/utils/routes.ts @@ -0,0 +1,126 @@ +export type AppRoute = + | { kind: 'home'; path: '/' } + | { kind: 'events'; path: '/events'; closeTo: '/' } + | { kind: 'byo'; path: '/byo' } + | { kind: 'byoEvents'; path: '/byo/events'; closeTo: '/byo' } + | { kind: 'node'; path: string; nodeId: string } + | { kind: 'nodeEvents'; path: string; nodeId: string; closeTo: string } + | { kind: 'snapshot'; path: string; frameId: string } + | { kind: 'snapshotEvents'; path: string; frameId: string; closeTo: string }; + +function normalizePath(path: string): string { + const pathname = path.split(/[?#]/, 1)[0] || '/'; + const withLeadingSlash = pathname.startsWith('/') ? pathname : `/${pathname}`; + return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash; +} + +function decodeSegment(segment: string): string { + try { + return decodeURIComponent(segment); + } catch { + return segment; + } +} + +/* Node ids are multi-segment paths (e.g. "ethpandaops/mainnet/node-001"): + * their slashes stay literal in URLs and each segment is encoded + * individually, so node URLs remain readable and deep-linkable. */ +function encodeNodeId(nodeId: string): string { + return nodeId.split('/').map(encodeURIComponent).join('/'); +} + +export function nodePath(nodeId: string): string { + return `/node/${encodeNodeId(nodeId)}`; +} + +export function nodeEventsPath(nodeId: string): string { + return `${nodePath(nodeId)}/events`; +} + +export function snapshotPath(frameId: string): string { + return `/snapshot/${encodeURIComponent(frameId)}`; +} + +export function snapshotEventsPath(frameId: string): string { + return `${snapshotPath(frameId)}/events`; +} + +/** + * Split the remainder of a /node/ URL into the node id and the optional + * /events overlay suffix. A trailing "events" segment always means the + * overlay (a node id whose final segment is literally "events" would be + * ambiguous; the overlay wins). + */ +export function parseNodeSplat( + splat: string | undefined, +): { nodeId: string; events: boolean } | undefined { + const segments = (splat ?? '').split('/').filter(Boolean); + if (segments.length === 0) return undefined; + const events = segments.length >= 2 && segments[segments.length - 1] === 'events'; + const nodeId = (events ? segments.slice(0, -1) : segments).map(decodeSegment).join('/'); + return { nodeId, events }; +} + +export function parseAppRoute(path: string): AppRoute | undefined { + const normalizedPath = normalizePath(path); + const segments = normalizedPath.split('/').filter(Boolean); + + if (segments.length === 0) return { kind: 'home', path: '/' }; + if (segments.length === 1 && segments[0] === 'events') { + return { kind: 'events', path: '/events', closeTo: '/' }; + } + if (segments.length === 1 && segments[0] === 'byo') return { kind: 'byo', path: '/byo' }; + if (segments.length === 2 && segments[0] === 'byo' && segments[1] === 'events') { + return { kind: 'byoEvents', path: '/byo/events', closeTo: '/byo' }; + } + if (segments.length >= 2 && segments[0] === 'node') { + const parsed = parseNodeSplat(segments.slice(1).join('/')); + if (!parsed) return undefined; + if (parsed.events) { + return { + kind: 'nodeEvents', + path: nodeEventsPath(parsed.nodeId), + nodeId: parsed.nodeId, + closeTo: nodePath(parsed.nodeId), + }; + } + return { kind: 'node', path: nodePath(parsed.nodeId), nodeId: parsed.nodeId }; + } + if (segments.length === 2 && segments[0] === 'snapshot') { + const frameId = decodeSegment(segments[1]); + return { kind: 'snapshot', path: snapshotPath(frameId), frameId }; + } + if (segments.length === 3 && segments[0] === 'snapshot' && segments[2] === 'events') { + const frameId = decodeSegment(segments[1]); + return { + kind: 'snapshotEvents', + path: snapshotEventsPath(frameId), + frameId, + closeTo: snapshotPath(frameId), + }; + } + + return undefined; +} + +export function eventsPathFor(path: string): string { + const route = parseAppRoute(path); + + switch (route?.kind) { + case 'home': + return '/events'; + case 'byo': + return '/byo/events'; + case 'node': + return nodeEventsPath(route.nodeId); + case 'snapshot': + return snapshotEventsPath(route.frameId); + case 'events': + case 'byoEvents': + case 'nodeEvents': + case 'snapshotEvents': + return route.path; + default: + return '/events'; + } +} diff --git a/web/src/utils/tailwind.ts b/web/src/utils/tailwind.ts deleted file mode 100644 index 457a450..0000000 --- a/web/src/utils/tailwind.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Technically, tailwind config could be imported and the colors be extracted from there. - * However this adds ~100kb to the production bundle size, so we'll just hardcode them here - * for colors that are used in canvas/webgl components. - * - * https://tailwindcss.com/docs/configuration#referencing-in-java-script - */ - -export const colors = { - stone: { - 50: '#fafaf9', - 600: '#57534e', - 700: '#44403c', - 800: '#292524', - 900: '#1c1917', - }, - emerald: { - 600: '#059669', - 800: '#065f46', - }, - amber: { - 600: '#d97706', - 800: '#92400e', - }, - indigo: { - 600: '#4f46e5', - 800: '#3730a3', - }, - fuchsia: { - 600: '#c026d3', - 800: '#86198f', - }, - rose: { - 600: '#e11d48', - 800: '#9f1239', - }, -}; - -export const fonts = { - mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - sans: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', - serif: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif', -}; diff --git a/web/static.go b/web/static.go deleted file mode 100644 index e796dd2..0000000 --- a/web/static.go +++ /dev/null @@ -1,6 +0,0 @@ -package static - -import "embed" - -//go:embed build/* -var FS embed.FS diff --git a/web/tailwind.config.js b/web/tailwind.config.js deleted file mode 100644 index 76f410d..0000000 --- a/web/tailwind.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: { - boxShadow: { - 'inner-lg': '0px 0px 5px 1px rgba(0,0,0,0.5) inset', - 'inner-xl': '0px 0px 10px 2px rgba(0,0,0,0.5) inset', - }, - keyframes: { - 'pulse-light': { - '50%': { opacity: 0.97 }, - }, - }, - animation: { - 'pulse-light': 'pulse-light 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', - }, - }, - }, - plugins: [], - darkMode: 'class', -}; diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json index 77233bc..984f56d 100644 --- a/web/tsconfig.app.json +++ b/web/tsconfig.app.json @@ -1,17 +1,11 @@ { "compilerOptions": { - "baseUrl": ".", "target": "ES2020", "useDefineForClassFields": true, - "lib": [ - "DOM", - "DOM.Iterable", - "ESNext" - ], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": false, "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", @@ -26,46 +20,20 @@ "noEmit": true, "jsx": "react-jsx", "paths": { - "@app/*": [ - "src/*" - ], - "@api/*": [ - "src/api/*" - ], - "@styles/*": [ - "src/styles/*" - ], - "@public/*": [ - "public/*" - ], - "@parts/*": [ - "src/parts/*" - ], - "@components/*": [ - "src/components/*" - ], - "@hooks/*": [ - "src/hooks/*" - ], - "@assets/*": [ - "src/assets/*" - ], - "@utils/*": [ - "src/utils/*" - ], - "@providers/*": [ - "src/providers/*" - ], - "@contexts/*": [ - "src/contexts/*" - ] + "@app/*": ["./src/*"], + "@api": ["./src/api"], + "@api/*": ["./src/api/*"], + "@styles/*": ["./src/styles/*"], + "@public/*": ["./public/*"], + "@parts/*": ["./src/parts/*"], + "@components/*": ["./src/components/*"], + "@hooks/*": ["./src/hooks/*"], + "@assets/*": ["./src/assets/*"], + "@utils/*": ["./src/utils/*"], + "@providers/*": ["./src/providers/*"], + "@contexts/*": ["./src/contexts/*"] } }, - "include": [ - "src", - "vitest.setup.ts" - ], - "exclude": [ - "src/**/__tests__/*" - ] + "include": ["src", "vitest.setup.ts"], + "exclude": ["./src/**/__tests__/*", "./src/**/*.stories.tsx"] } diff --git a/web/tsconfig.json b/web/tsconfig.json index 08c8a90..e09462f 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.storybook.json" }, { "path": "./tsconfig.vitest.json" } ] } diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json index 8e5b203..50be551 100644 --- a/web/tsconfig.node.json +++ b/web/tsconfig.node.json @@ -14,5 +14,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "vitest.config.ts", "vitest.config.unit.ts", ".storybook/main.ts"] } diff --git a/web/tsconfig.storybook.json b/web/tsconfig.storybook.json new file mode 100644 index 0000000..b9447a5 --- /dev/null +++ b/web/tsconfig.storybook.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "composite": true, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src", ".storybook/**/*.ts", ".storybook/**/*.tsx"], + "exclude": ["node_modules", "./src/**/__tests__/*", "./src/index.tsx"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts index 19bb7a9..16aa574 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,12 +1,24 @@ +import { tanstackRouter } from '@tanstack/router-plugin/vite'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import tailwindcss from '@tailwindcss/vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ - plugins: [react(), tsconfigPaths(), tailwindcss()], + plugins: [ + tanstackRouter({ + routesDirectory: './src/routes', + generatedRouteTree: './src/routeTree.gen.ts', + autoCodeSplitting: false, + quoteStyle: 'single', + }), + react(), + tailwindcss(), + ], + resolve: { + tsconfigPaths: true, + }, build: { - outDir: 'build/frontend', + outDir: 'dist', }, server: { proxy: { diff --git a/web/vitest.config.ts b/web/vitest.config.ts index 8a30e6a..d85d38b 100644 --- a/web/vitest.config.ts +++ b/web/vitest.config.ts @@ -1,20 +1,70 @@ -import tsconfigPaths from 'vite-tsconfig-paths'; +import path from 'node:path'; + +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; import { defineConfig, mergeConfig } from 'vitest/config'; -import GithubActionsReporter from 'vitest-github-actions-reporter'; + import viteConfig from './vite.config.js'; export default mergeConfig( viteConfig, defineConfig({ - plugins: [tsconfigPaths()], + plugins: [ + storybookTest({ + configDir: path.join(__dirname, '.storybook'), + tags: { + exclude: ['test-exclude'], + }, + }), + ], test: { + name: 'storybook', globals: true, environment: 'jsdom', - setupFiles: 'vitest.setup.ts', - reporters: process.env.GITHUB_ACTIONS - ? ['default', new GithubActionsReporter()] - : ['default'], - coverage: {}, + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [ + { + browser: 'chromium', + }, + ], + }, + setupFiles: ['./.storybook/vitest-setup.ts'], + }, + optimizeDeps: { + include: [ + 'react', + 'react-dom', + 'react-dom/client', + 'react/jsx-dev-runtime', + 'react/jsx-runtime', + '@headlessui/react', + '@heroicons/react/20/solid', + '@heroicons/react/24/solid', + '@tanstack/react-query', + '@tanstack/react-router', + 'clsx', + 'graphology', + 'javascript-time-ago', + 'react-time-ago', + 'react-zoom-pan-pinch', + 'zod', + 'zod/mini', + '@testing-library/jest-dom', + '@storybook/addon-docs', + '@storybook/addon-docs/blocks', + 'msw-storybook-addon', + 'storybook/internal/channels', + 'storybook/internal/client-logger', + 'storybook/internal/docs-tools', + 'storybook/internal/preview-api', + 'storybook/internal/preview/runtime', + 'storybook/preview-api', + 'storybook/test', + 'storybook/viewport', + ], }, }), ); diff --git a/web/vitest.config.unit.ts b/web/vitest.config.unit.ts new file mode 100644 index 0000000..5dfb97b --- /dev/null +++ b/web/vitest.config.unit.ts @@ -0,0 +1,22 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; + +import viteConfig from './vite.config.js'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + name: 'unit', + globals: true, + environment: 'jsdom', + include: ['src/**/*.test.{ts,tsx}'], + exclude: ['src/**/*.stories.{ts,tsx}', 'node_modules'], + setupFiles: ['./vitest.setup.ts'], + reporters: process.env.GITHUB_ACTIONS ? ['default', 'github-actions'] : ['default'], + coverage: {}, + }, + optimizeDeps: { + include: ['react-dom/client', 'zod/mini'], + }, + }), +);