From 9af92af5fd7ff72cc07e5915b003081878e0985a Mon Sep 17 00:00:00 2001 From: Boris Zhilko Date: Thu, 24 Sep 2026 11:24:17 +0500 Subject: [PATCH] v1 go package --- .dockerignore | 7 + .github/dependabot.yml | 8 + .github/workflows/ci.yml | 70 ++ .github/workflows/release.yml | 55 ++ .gitignore | 33 + .golangci.yml | 33 + README.md | 103 ++ RELEASING.md | 22 + SECURITY.md | 21 + example/.env.local.example | 3 + example/.gitignore | 45 + example/README.md | 60 ++ example/backend/main.go | 117 +++ example/index.html | 12 + example/package-lock.json | 1742 +++++++++++++++++++++++++++++++++ example/package.json | 23 + example/run.sh | 21 + example/src/App.tsx | 66 ++ example/src/main.tsx | 5 + example/src/styles.css | 59 ++ example/tsconfig.json | 25 + example/vite.config.ts | 28 + go.mod | 3 + hcaptcha/example_test.go | 39 + hcaptcha/hcaptcha.go | 128 +++ hcaptcha/hcaptcha_test.go | 263 +++++ hcaptcha/integration_test.go | 23 + 27 files changed, 3014 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 README.md create mode 100644 RELEASING.md create mode 100644 SECURITY.md create mode 100644 example/.env.local.example create mode 100644 example/.gitignore create mode 100644 example/README.md create mode 100644 example/backend/main.go create mode 100644 example/index.html create mode 100644 example/package-lock.json create mode 100644 example/package.json create mode 100755 example/run.sh create mode 100644 example/src/App.tsx create mode 100644 example/src/main.tsx create mode 100644 example/src/styles.css create mode 100644 example/tsconfig.json create mode 100644 example/vite.config.ts create mode 100644 go.mod create mode 100644 hcaptcha/example_test.go create mode 100644 hcaptcha/hcaptcha.go create mode 100644 hcaptcha/hcaptcha_test.go create mode 100644 hcaptcha/integration_test.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2dea87f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +.env +**/.env +coverage.* +*.out +*.test diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b3ae468 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..db0dcd6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + minimum-go: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Set up minimum supported Go version + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: "1.24.x" + cache-dependency-path: go.mod + + - name: Test package + env: + GOTOOLCHAIN: local + run: go test ./... + + quality: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: "1.26.x" + check-latest: true + cache-dependency-path: go.mod + + - name: Verify formatting and module metadata + run: | + test -z "$(gofmt -l .)" + go mod tidy -diff + go mod verify + + - name: Check workflow syntax + run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + + - name: Lint package + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 + with: + version: v2.11.3 + args: --config=.golangci.yml --timeout=5m ./... + + - name: Test package + run: | + go vet ./... + go test -race -shuffle=on -count=1 ./... + go test -covermode=atomic -coverprofile=coverage.out ./hcaptcha + coverage="$(go tool cover -func=coverage.out | awk '/^total:/ {gsub(/%/, "", $3); print $3}')" + awk -v coverage="$coverage" 'BEGIN { if (coverage + 0 < 80) exit 1 }' + + - name: Scan dependency graph + run: go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ddcdd31 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,55 @@ +name: Release verification + +on: + push: + tags: + - "v*" + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Check out the release tag + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + + - name: Validate release tag + env: + GH_TOKEN: ${{ github.token }} + run: | + semver_pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' + [[ "$GITHUB_REF_NAME" =~ $semver_pattern ]] + test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = tag + tag_object_sha="$(git rev-parse "refs/tags/$GITHUB_REF_NAME")" + tag_object="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_object_sha")" + jq -e '.verification.verified == true' >/dev/null <<<"$tag_object" + git fetch origin main + git merge-base --is-ancestor "$(git rev-list -n 1 "$GITHUB_REF_NAME")" origin/main + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: "1.26.x" + check-latest: true + cache-dependency-path: go.mod + + - name: Verify package + run: | + go mod verify + go test -race -count=1 ./... + + - name: Run documented live Siteverify test + env: + HCAPTCHA_LIVE_TEST: "1" + run: go test -count=1 -run '^TestLiveSiteverify$' ./hcaptcha + + - name: Scan dependency graph + run: go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2942f36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ +.DS_Store diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..1df6563 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,33 @@ +version: "2" + +run: + timeout: 5m + modules-download-mode: readonly + +linters: + default: none + enable: + - bodyclose + - errcheck + - errorlint + - gocritic + - gosec + - govet + - ineffassign + - misspell + - noctx + - revive + - staticcheck + - unconvert + - unparam + exclusions: + rules: + # Test-only TLS handlers parse bounded, in-memory request bodies. + - path: _test\.go + text: G120 + linters: + - gosec + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8db94e3 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# hCaptcha Go integration + +Small Go client for [hCaptcha Siteverify](https://docs.hcaptcha.com/#verify-the-user-response-server-side). It verifies a response token on your server after the browser completes hCaptcha; it does not render the widget or make authorization decisions for you. + +## Install + +```sh +go get github.com/hCaptcha/hcaptcha-go/hcaptcha +``` + +Requires Go 1.24 or later. + +## Use + +Create a client once with an hCaptcha service secret, then reuse it across handlers. Keep the secret only in server-side configuration. + +```go +package main + +import ( + "net/http" + + hcaptcha "github.com/hCaptcha/hcaptcha-go/hcaptcha" +) + +var verifier, err = hcaptcha.New("server-side-secret") + +func protected(w http.ResponseWriter, r *http.Request) { + if err != nil { + http.Error(w, "verification unavailable", http.StatusInternalServerError) + return + } + if err := r.ParseForm(); err != nil { + http.Error(w, "invalid form", http.StatusBadRequest) + return + } + result, err := verifier.VerifyRequest(r.Context(), hcaptcha.Request{ + Token: r.FormValue("h-captcha-response"), + SiteKey: "expected-sitekey-uuid", // Bind token to this sitekey. + }) + if err != nil { + http.Error(w, "verification unavailable", http.StatusBadGateway) + return + } + if result["success"] != true { + http.Error(w, "verification failed", http.StatusForbidden) + return + } + + // Perform the protected action. +} +``` + +Use `Verify(token)` for the minimal case. `VerifyContext(ctx, token)` and `VerifyRequest(ctx, request)` propagate handler cancellation. `Request.Token` is required; `RemoteIP` and `SiteKey` are optional parameters sent only when non-empty. + +`Result` is `map[string]any`, retaining all fields returned by Siteverify. Treat a transport or decode `error` separately from an unsuccessful response, and fail closed in both cases. Check `success` before protected work. Inspect `error-codes` for operational logging; do not expose raw verification details to users. + +## Browser integration + +This package receives the token generated by a browser or mobile integration. Add the hCaptcha widget, submit its `h-captcha-response` field to your backend, then call this package from that backend. + +```html + +
+``` + +Use the [Developer Guide](https://docs.hcaptcha.com/) for the complete browser-to-server flow and the [JavaScript configuration reference](https://docs.hcaptcha.com/configuration) for explicit rendering, callbacks, themes, and client-side errors. + +## Security and operations + +- Call Siteverify from the backend with a form-encoded POST. Do not expose the secret or accept a client-side success claim. +- Verify tokens promptly and exactly once. hCaptcha tokens are short-lived and single-use. +- Pass the expected `SiteKey` to prevent a token issued for another sitekey from being accepted. `hostname` is informational, not an authentication control. +- Send `RemoteIP` when available, using an IP derived from a trusted-proxy policy. Do not trust forwarding headers from arbitrary clients. +- Configure the hCaptcha domain allowlist and keep the secret in a secret manager or environment variable. [Rotate a leaked secret](https://docs.hcaptcha.com/#rotating-your-siteverify-secret) immediately. + +See hCaptcha’s [server-side verification guidance](https://docs.hcaptcha.com/#verify-the-user-response-server-side), [domain allowlist configuration](https://docs.hcaptcha.com/configuration#domain-allowlist), and [Siteverify error-code reference](https://docs.hcaptcha.com/#siteverify-error-codes-table). + + +## Example and testing + +Run the included React/Vite browser and Go backend example: + +```sh +cd example +cp .env.local.example .env.local +npm install +./run.sh +``` + +The sample env uses [hCaptcha's public test keys](https://docs.hcaptcha.com/#integration-testing-test-keys), +which skip visual challenges. To use your own keys locally, follow +[the example's local hostname setup](example/README.md#run). hCaptcha's +[local development guide](https://docs.hcaptcha.com/#local-development) +lists `localhost` and `127.0.0.1` as unsupported hostnames. In this +example, a real-key challenge can still pass there after several checkbox +attempts, but that flow is unreliable. + +Run package tests with: + +```sh +go test ./... +``` diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..0144b89 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,22 @@ +# Releasing + +This package follows semantic versioning. Releases are Go module tags such as +`v1.0.0`; there are no binary artifacts. + +1. Confirm the intended commit is on the default branch and CI is green. +2. Review dependency, vulnerability, documentation, and live-test results. +3. Confirm installation examples and Siteverify documentation match the + release. +4. Create a signed annotated semantic-version tag from the reviewed default + branch commit and push that tag. The release-verification workflow rejects + unsigned, lightweight, invalid semantic-version, or non-default-branch tags + and reruns live and dependency checks. +5. Confirm release verification succeeds, then verify the version through the + Go module proxy and pkg.go.dev. +6. Create the corresponding GitHub release notes. +7. Submit the released package to the public hCaptcha integrations list. + +Do not tag from a dirty worktree or move an existing release tag. If a release +is incorrect, publish a new patch version instead. Keep the default branch +protected, require CI and an approving maintainer review, and retain release +provenance with the GitHub release. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..dd61f4c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security policy + +## Supported versions + +Before the first tagged release, security fixes are applied to the latest +commit on the default branch. After release, the latest supported major version +receives security fixes. + +## Reporting a vulnerability + +Do not open a public GitHub issue for a suspected vulnerability. + +Email `support@hcaptcha.com` and copy `security@hcaptcha.com`. Include the +affected version or commit, impact, reproduction steps, and any proposed +mitigation. Do not include live hCaptcha secrets, response tokens, customer +data, or other credentials. Use only accounts and systems you are authorized to +test, avoid privacy violations and service disruption, and allow time for +coordinated remediation before disclosure. + +For ordinary integration questions and non-security bugs, use the public issue +tracker after the repository is released. diff --git a/example/.env.local.example b/example/.env.local.example new file mode 100644 index 0000000..72befb8 --- /dev/null +++ b/example/.env.local.example @@ -0,0 +1,3 @@ +VITE_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001 +HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000 +GO_BACKEND_URL=http://127.0.0.1:8080 diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..f84665d --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,45 @@ +# Dependencies +/node_modules/ +/.pnp +.pnp.* +/.yarn/* +!/.yarn/patches +!/.yarn/plugins +!/.yarn/releases +!/.yarn/versions + +# Build output +/dist/ + +# Test output +/coverage/ +coverage.* +*.coverprofile +*.test + +# Environment files +.env* +!.env.local.example + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Hosting +/.vercel/ + +# TypeScript +*.tsbuildinfo + +# Local Go binaries +/example-server +/server + +# OS and editor state +.DS_Store +.idea/ +.vscode/ +*.swp diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..3a2e10f --- /dev/null +++ b/example/README.md @@ -0,0 +1,60 @@ +# End-to-end example + +This application exercises the checked-out Go package through a React/Vite UI +with [hCaptcha's React component](https://docs.hcaptcha.com/integrations/react). +Vite proxies `/api/protected` to the loopback-only Go backend, so browser requests +remain same-origin. + +## Run + +```sh +cd example +cp .env.local.example .env.local +npm install +./run.sh +``` + +hCaptcha's [local development guide](https://docs.hcaptcha.com/#local-development) +lists `localhost` and `127.0.0.1` as unsupported hostnames. On a loopback URL, +this example can still show a challenge and pass Siteverify, though users may +need to click the checkbox a few times. The hostname warning makes that flow +unreliable. To solve challenges locally without the warning: + +1. Choose a development hostname under a domain you control, such as + `captcha-dev.example.com` (replace this placeholder with your own hostname). +2. Map it to loopback in your hosts file (`/private/etc/hosts` on macOS, + `/etc/hosts` on Linux, or `C:\Windows\System32\Drivers\etc\hosts` on Windows): + + ```text + 127.0.0.1 captcha-dev.example.com + ``` + +3. Add `ALLOWED_HOSTS=captcha-dev.example.com` to `.env.local`. If + [domain allowlisting](https://docs.hcaptcha.com/configuration#domain-allowlist) + is enabled for your hCaptcha sitekey, add the same hostname there. +4. Run `./run.sh` and open `http://captcha-dev.example.com:3000`. + +The [public test credentials](https://docs.hcaptcha.com/#integration-testing-test-keys) +in `.env.local.example` validate integration wiring. They never show a visual +challenge and provide no anti-bot protection. Use your own sitekey and matching +secret to exercise a real challenge. For a reverse proxy, add its public hostname +to `ALLOWED_HOSTS` and, when enabled, the sitekey's domain allowlist. If hCaptcha +reports [`network-error`](https://docs.hcaptcha.com/configuration#error-codes) +on the mapped hostname, inspect browser requests and blockers. + +The backend binds to `127.0.0.1:8080`. It accepts `X-Forwarded-For` only from a +loopback peer (the Vite proxy), sends that IP and the expected sitekey to +[Siteverify](https://docs.hcaptcha.com/#verify-the-user-response-server-side), +and returns the full Siteverify result for inspection. Do not copy +that diagnostic response behavior into a production endpoint. + +## Expected checks + +- Completing a challenge returns HTTP 200 and `siteverify.success: true`. +- An invalid token returns HTTP 403. With real keys, reusing a token also + returns HTTP 403; the public test token always verifies successfully with + its matching test secret. +- Submitting without a token is disabled in the UI; a direct empty request + returns HTTP 400. +- Stopping the Go backend causes the proxy request to fail, proving verification + is server-side and fail-closed. diff --git a/example/backend/main.go b/example/backend/main.go new file mode 100644 index 0000000..df64559 --- /dev/null +++ b/example/backend/main.go @@ -0,0 +1,117 @@ +// Package main runs the Go backend for the browser example. +package main + +import ( + "encoding/json" + "errors" + "log" + "net" + "net/http" + "os" + "strings" + "time" + + hcaptcha "github.com/hCaptcha/hcaptcha-go/hcaptcha" +) + +const maxRequestBodyBytes = 16 << 10 + +type application struct { + verifier *hcaptcha.Client + siteKey string +} + +func main() { + secret := strings.TrimSpace(os.Getenv("HCAPTCHA_SECRET")) + siteKey := strings.TrimSpace(os.Getenv("VITE_HCAPTCHA_SITEKEY")) + if secret == "" || siteKey == "" { + log.Fatal("HCAPTCHA_SECRET and VITE_HCAPTCHA_SITEKEY are required") + } + + verifier, err := hcaptcha.New(secret) + if err != nil { + log.Fatal(err) + } + app := application{verifier: verifier, siteKey: siteKey} + + mux := http.NewServeMux() + mux.HandleFunc("GET /health", health) + mux.HandleFunc("POST /protected", app.protected) + server := &http.Server{ + Addr: "127.0.0.1:8080", + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + log.Printf("Go backend listening on %s", server.Addr) + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatal(err) + } +} + +func health(w http.ResponseWriter, _ *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{"ok": true}) +} + +func (app application) protected(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodyBytes) + // This example reads the token from a form POST. JSON APIs can decode the body instead. + if err := r.ParseForm(); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid form"}) + return + } + token := strings.TrimSpace(r.FormValue("h-captcha-response")) + if token == "" { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": "valid token is required"}) + return + } + + result, err := app.verifier.VerifyRequest(r.Context(), hcaptcha.Request{ + Token: token, + RemoteIP: clientIP(r), + SiteKey: app.siteKey, + }) + if err != nil { + log.Printf("siteverify call failed: %v", err) + writeJSON(w, http.StatusBadGateway, map[string]any{"error": "siteverify unavailable"}) + return + } + + if result["success"] != true { + writeJSON(w, http.StatusForbidden, map[string]any{"siteverify": result}) + return + } + + // Perform the protected action. + writeJSON(w, http.StatusOK, map[string]any{"siteverify": result}) +} + +func clientIP(r *http.Request) string { + peer, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return "" + } + peerIP := net.ParseIP(peer) + if peerIP == nil || !peerIP.IsLoopback() { + return peer + } + + forwarded := strings.TrimSpace(strings.Split(r.Header.Get("X-Forwarded-For"), ",")[0]) + if net.ParseIP(forwarded) != nil { + return forwarded + } + return peer +} + +func writeJSON(w http.ResponseWriter, status int, value any) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(value); err != nil { + log.Printf("encode response: %v", err) + } +} diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..541bf99 --- /dev/null +++ b/example/index.html @@ -0,0 +1,12 @@ + + + + + + hCaptcha Go integration example + + +
+ + + diff --git a/example/package-lock.json b/example/package-lock.json new file mode 100644 index 0000000..56615ce --- /dev/null +++ b/example/package-lock.json @@ -0,0 +1,1742 @@ +{ + "name": "hcaptcha-go-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hcaptcha-go-example", + "dependencies": { + "@hcaptcha/react-hcaptcha": "2.2.0", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@types/node": "^20.19.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "4.7.0", + "typescript": "^5.9.0", + "vite": "6.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hcaptcha/react-hcaptcha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-2.2.0.tgz", + "integrity": "sha512-/HrPgNLRrJziLrh2s+qFRO33ubBSuHn9sdNsQcP0wQElpORxlpR6wnq3X9j/bZGd+uQtYIvPl4QSU2la+Jl9Hg==" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", + "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", + "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", + "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", + "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", + "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", + "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", + "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", + "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", + "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", + "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", + "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", + "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", + "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", + "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", + "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", + "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", + "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", + "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", + "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", + "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", + "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", + "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", + "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", + "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", + "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "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, + "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, + "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, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@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 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "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" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "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 + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.434", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.434.tgz", + "integrity": "sha512-7EeFW9OLf1NN9NKQP9xdOE/XKqjxv7JDPP6AEAkHli11+Fae1OaLoNuAf13hQv1PPNtuES0pLQSPk2+fS3s8ww==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": 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, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.56", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz", + "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", + "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.4", + "@rollup/rollup-android-arm64": "4.63.4", + "@rollup/rollup-darwin-arm64": "4.63.4", + "@rollup/rollup-darwin-x64": "4.63.4", + "@rollup/rollup-freebsd-arm64": "4.63.4", + "@rollup/rollup-freebsd-x64": "4.63.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", + "@rollup/rollup-linux-arm-musleabihf": "4.63.4", + "@rollup/rollup-linux-arm64-gnu": "4.63.4", + "@rollup/rollup-linux-arm64-musl": "4.63.4", + "@rollup/rollup-linux-loong64-gnu": "4.63.4", + "@rollup/rollup-linux-loong64-musl": "4.63.4", + "@rollup/rollup-linux-ppc64-gnu": "4.63.4", + "@rollup/rollup-linux-ppc64-musl": "4.63.4", + "@rollup/rollup-linux-riscv64-gnu": "4.63.4", + "@rollup/rollup-linux-riscv64-musl": "4.63.4", + "@rollup/rollup-linux-s390x-gnu": "4.63.4", + "@rollup/rollup-linux-x64-gnu": "4.63.4", + "@rollup/rollup-linux-x64-musl": "4.63.4", + "@rollup/rollup-openbsd-x64": "4.63.4", + "@rollup/rollup-openharmony-arm64": "4.63.4", + "@rollup/rollup-win32-arm64-msvc": "4.63.4", + "@rollup/rollup-win32-ia32-msvc": "4.63.4", + "@rollup/rollup-win32-x64-gnu": "4.63.4", + "@rollup/rollup-win32-x64-msvc": "4.63.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==" + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "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" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..22f6d51 --- /dev/null +++ b/example/package.json @@ -0,0 +1,23 @@ +{ + "name": "hcaptcha-go-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@hcaptcha/react-hcaptcha": "2.2.0", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@types/node": "^20.19.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "4.7.0", + "typescript": "^5.9.0", + "vite": "6.4.3" + } +} diff --git a/example/run.sh b/example/run.sh new file mode 100755 index 0000000..c08f023 --- /dev/null +++ b/example/run.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" +if [ ! -f .env.local ]; then + echo "Copy .env.local.example to .env.local and set credentials." >&2 + exit 1 +fi + +set -a +. ./.env.local +set +a + +cleanup() { + kill "$backend_pid" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +go run ./backend & +backend_pid=$! +npm run dev diff --git a/example/src/App.tsx b/example/src/App.tsx new file mode 100644 index 0000000..0ce202d --- /dev/null +++ b/example/src/App.tsx @@ -0,0 +1,66 @@ +import HCaptcha from "@hcaptcha/react-hcaptcha"; +import { useRef, useState } from "react"; +import type { FormEvent } from "react"; + +const siteKey = import.meta.env.VITE_HCAPTCHA_SITEKEY; + +export function App() { + const captcha = useRef(null); + const [token, setToken] = useState(""); + const [pending, setPending] = useState(false); + const [result, setResult] = useState(null); + const [widgetError, setWidgetError] = useState(""); + + async function submit(event: FormEvent) { + event.preventDefault(); + setPending(true); + setResult(null); + + try { + const response = await fetch("/api/protected", { + method: "POST", + body: new URLSearchParams({ "h-captcha-response": token }), + }); + const body = await response.json(); + setResult({ httpStatus: response.status, ...body }); + } catch (error) { + setResult({ error: error instanceof Error ? error.message : String(error) }); + } finally { + setPending(false); + setToken(""); + captcha.current?.resetCaptcha(); + } + } + + return ( +
+

PACKAGE E2E EXAMPLE

+

hCaptcha Go integration

+

+ Complete the challenge, then send the token through the Vite proxy to + the Go backend. The response below is the package's unmodified + Siteverify result. +

+ {!siteKey &&

VITE_HCAPTCHA_SITEKEY is missing.

} + {widgetError &&

{widgetError}

} + {siteKey && ( +
+ setToken("")} + onError={(error) => { + setToken(""); + setWidgetError(String(error)); + }} + /> + + + )} + {result !== null &&
{JSON.stringify(result, null, 2)}
} +
+ ); +} diff --git a/example/src/main.tsx b/example/src/main.tsx new file mode 100644 index 0000000..bbc4d24 --- /dev/null +++ b/example/src/main.tsx @@ -0,0 +1,5 @@ +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render(); diff --git a/example/src/styles.css b/example/src/styles.css new file mode 100644 index 0000000..960e82c --- /dev/null +++ b/example/src/styles.css @@ -0,0 +1,59 @@ +:root { + color-scheme: light dark; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +body { + margin: 0; +} + +main { + margin: 10vh auto; + max-width: 44rem; + padding: 0 1.25rem; +} + +h1 { + font-size: clamp(2rem, 7vw, 4rem); + letter-spacing: -0.06em; + line-height: 0.95; +} + +p { + line-height: 1.6; +} + +.eyebrow { + font-size: 0.75rem; + letter-spacing: 0.14em; + opacity: 0.65; +} + +form { + display: grid; + gap: 1rem; + justify-items: start; + margin-top: 2rem; +} + +button { + cursor: pointer; + font: inherit; + padding: 0.7rem 1rem; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +pre { + border: 1px solid color-mix(in srgb, currentColor 25%, transparent); + margin-top: 2rem; + overflow: auto; + padding: 1rem; +} + +.error { + color: #d33; +} diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000..d4e5e1d --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "types": ["vite/client", "node"] + }, + "include": ["src", "vite.config.ts"], + "exclude": [ + "node_modules" + ] +} diff --git a/example/vite.config.ts b/example/vite.config.ts new file mode 100644 index 0000000..f438fe5 --- /dev/null +++ b/example/vite.config.ts @@ -0,0 +1,28 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig, loadEnv } from "vite"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, ".", ""); + const extraHosts = env.ALLOWED_HOSTS?.split(",").map((host) => host.trim()).filter(Boolean) ?? []; + const proxy = { + "/api": { + target: env.GO_BACKEND_URL || "http://127.0.0.1:8080", + changeOrigin: false, + xfwd: true, + rewrite: (path: string) => path.replace(/^\/api/, ""), + }, + }; + const server = { + host: "127.0.0.1", + port: 3000, + strictPort: true, + allowedHosts: extraHosts, + proxy, + }; + + return { + plugins: [react()], + server, + preview: server, + }; +}); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..627035f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/hCaptcha/hcaptcha-go + +go 1.24 diff --git a/hcaptcha/example_test.go b/hcaptcha/example_test.go new file mode 100644 index 0000000..bc5f17e --- /dev/null +++ b/hcaptcha/example_test.go @@ -0,0 +1,39 @@ +package hcaptcha_test + +import ( + "context" + "os" + + hcaptcha "github.com/hCaptcha/hcaptcha-go/hcaptcha" +) + +func ExampleClient_Verify() { + secret := os.Getenv("HCAPTCHA_SECRET") + token := "response token from the application request" + verifier, err := hcaptcha.New(secret) + if err != nil { + return + } + + result, err := verifier.Verify(token) + if err != nil || result["success"] != true { + // Fail closed: do not continue with the protected action. + return + } + // Continue with the protected action. +} + +func ExampleClient_VerifyRequest() { + verifier, err := hcaptcha.New(os.Getenv("HCAPTCHA_SECRET")) + if err != nil { + return + } + result, err := verifier.VerifyRequest(context.Background(), hcaptcha.Request{ + Token: "response token from the application request", + RemoteIP: "203.0.113.1", + SiteKey: "expected sitekey UUID", + }) + if err != nil || result["success"] != true { + return + } +} diff --git a/hcaptcha/hcaptcha.go b/hcaptcha/hcaptcha.go new file mode 100644 index 0000000..b93d484 --- /dev/null +++ b/hcaptcha/hcaptcha.go @@ -0,0 +1,128 @@ +// Package hcaptcha verifies hCaptcha response tokens with Siteverify. +package hcaptcha + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "time" +) + +const ( + siteverifyEndpoint = "https://api.hcaptcha.com/siteverify" + defaultTimeout = time.Second +) + +// Result contains the Siteverify response. The basic response includes success +// and error-codes; see https://docs.hcaptcha.com/#verify-the-user-response-server-side. +// +// For example, the usual success check is: +// +// if result["success"] == true { ... } +type Result map[string]any + +// Request contains a Siteverify response token and its optional request data. +// Token is required; RemoteIP and SiteKey are recommended. The account secret +// is passed to New, not included here. +type Request struct { + // Required. + Token string + + // Recommended. + RemoteIP string + SiteKey string +} + +// Client verifies tokens using the secret passed to New. A Client is safe to +// reuse concurrently by multiple HTTP handlers. +type Client struct { + secret string + endpoint string + httpClient *http.Client +} + +// New creates a verifier for one non-empty hCaptcha secret. +func New(secret string) (*Client, error) { + return NewWithHTTPClient(secret, &http.Client{Timeout: defaultTimeout}) +} + +// NewWithHTTPClient creates a verifier using httpClient without modifying it. +func NewWithHTTPClient(secret string, httpClient *http.Client) (*Client, error) { + if secret == "" { + return nil, errors.New("hcaptcha: secret is required") + } + if httpClient == nil { + return nil, errors.New("hcaptcha: http client is required") + } + return newClient(secret, siteverifyEndpoint, httpClient), nil +} + +func newClient(secret, endpoint string, httpClient *http.Client) *Client { + clientCopy := &http.Client{ + Transport: httpClient.Transport, + Jar: httpClient.Jar, + Timeout: httpClient.Timeout, + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + } + return &Client{ + secret: secret, endpoint: endpoint, httpClient: clientCopy, + } +} + +// Verify verifies token using a background context. +func (client *Client) Verify(token string) (Result, error) { + return client.VerifyContext(context.Background(), token) +} + +// VerifyContext verifies token with ctx. Use it in an HTTP handler to stop +// the Siteverify request when the incoming request is cancelled. +func (client *Client) VerifyContext(ctx context.Context, token string) (Result, error) { + return client.VerifyRequest(ctx, Request{Token: token}) +} + +// VerifyRequest verifies a token and optional Siteverify request data. +func (client *Client) VerifyRequest(ctx context.Context, request Request) (Result, error) { + if request.Token == "" { + return nil, errors.New("hcaptcha: token is required") + } + form := url.Values{"secret": {client.secret}, "response": {request.Token}} + setString(form, "remoteip", request.RemoteIP) + setString(form, "sitekey", request.SiteKey) + return client.call(ctx, client.endpoint, form) +} + +func setString(form url.Values, name, value string) { + if value != "" { + form.Set(name, value) + } +} + +func (client *Client) call(ctx context.Context, endpoint string, form url.Values) (Result, error) { + if ctx == nil { + ctx = context.Background() + } + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBufferString(form.Encode())) + if err != nil { + return nil, fmt.Errorf("hcaptcha: create request: %w", err) + } + httpRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") + httpRequest.Header.Set("Accept", "application/json") + + response, err := client.httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("hcaptcha: call siteverify: %w", err) + } + defer func() { _ = response.Body.Close() }() + + result := Result{} + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("hcaptcha: decode siteverify response: %w", err) + } + return result, nil +} diff --git a/hcaptcha/hcaptcha_test.go b/hcaptcha/hcaptcha_test.go new file mode 100644 index 0000000..a63cfb6 --- /dev/null +++ b/hcaptcha/hcaptcha_test.go @@ -0,0 +1,263 @@ +package hcaptcha + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestVerifyRequestEncodesAllFields(t *testing.T) { + request := Request{ + Token: "token", RemoteIP: "203.0.113.1", SiteKey: "sitekey", + } + want := url.Values{ + "secret": {"secret"}, "response": {"token"}, "remoteip": {"203.0.113.1"}, "sitekey": {"sitekey"}, + } + server := formServer(t, want) + defer server.Close() + + result, err := newClient("secret", server.URL, server.Client()).VerifyRequest(context.TODO(), request) + if err != nil { + t.Fatal(err) + } + if result["success"] != true { + t.Errorf("result = %#v", result) + } +} + +func TestNew(t *testing.T) { + client, err := New("secret") + if err != nil { + t.Fatal(err) + } + if got := client.httpClient.Timeout; got != defaultTimeout { + t.Errorf("timeout = %v, want %v", got, defaultTimeout) + } +} + +func TestNewRejectsInvalidConfiguration(t *testing.T) { + for _, test := range []struct { + name string + construct func() (*Client, error) + want string + }{ + {"empty secret", func() (*Client, error) { return New("") }, "hcaptcha: secret is required"}, + {"nil HTTP client", func() (*Client, error) { return NewWithHTTPClient("secret", nil) }, "hcaptcha: http client is required"}, + } { + t.Run(test.name, func(t *testing.T) { + _, err := test.construct() + if err == nil || err.Error() != test.want { + t.Errorf("error = %v, want %q", err, test.want) + } + }) + } +} + +func TestVerifyRejectsEmptyTokenBeforeRequest(t *testing.T) { + var calls atomic.Int32 + client, err := NewWithHTTPClient("secret", &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { + calls.Add(1) + return nil, errors.New("unexpected request") + })}) + if err != nil { + t.Fatal(err) + } + _, err = client.Verify("") + if err == nil || err.Error() != "hcaptcha: token is required" { + t.Errorf("error = %v, want token-required error", err) + } + if got := calls.Load(); got != 0 { + t.Errorf("HTTP calls = %d, want 0", got) + } +} + +func TestVerifyAcceptsEvolvingResponse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusTeapot) + _, _ = io.WriteString(w, `{"success":"now-a-string","new":{"nested":[1,true,null]},"number":1.5}`) + })) + defer server.Close() + result, err := newClient("secret", server.URL, server.Client()).Verify("token") + if err != nil { + t.Fatal(err) + } + if got, want := result["success"], "now-a-string"; got != want { + t.Errorf("success = %#v, want %#v", got, want) + } + if _, ok := result["new"].(map[string]any); !ok { + t.Errorf("new = %#v, want object", result["new"]) + } +} + +func TestVerifyDoesNotFollowRedirects(t *testing.T) { + var targetCalls atomic.Int32 + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + targetCalls.Add(1) + http.Error(w, "redirect target must not be called", http.StatusInternalServerError) + })) + defer target.Close() + origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", target.URL) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTemporaryRedirect) + _, _ = io.WriteString(w, `{"success":true,"redirect":true}`) + })) + defer origin.Close() + + result, err := newClient("secret", origin.URL, origin.Client()).Verify("token") + if err != nil { + t.Fatal(err) + } + if result["success"] != true || result["redirect"] != true { + t.Errorf("result = %#v", result) + } + if got := targetCalls.Load(); got != 0 { + t.Errorf("redirect target calls = %d, want 0", got) + } +} + +func TestNewWithHTTPClientPreservesConfiguration(t *testing.T) { + sentinel := errors.New("original redirect handler") + source := &http.Client{ + Transport: http.DefaultTransport, + Timeout: 3 * time.Second, + CheckRedirect: func(*http.Request, []*http.Request) error { return sentinel }, + } + client, err := NewWithHTTPClient("secret", source) + if err != nil { + t.Fatal(err) + } + if err := source.CheckRedirect(nil, nil); !errors.Is(err, sentinel) { + t.Errorf("source redirect handler changed: %v", err) + } + if client.httpClient == source { + t.Error("HTTP client was not copied") + } + if client.httpClient.Transport != source.Transport || client.httpClient.Timeout != source.Timeout { + t.Error("HTTP client configuration was not preserved") + } + if err := client.httpClient.CheckRedirect(nil, nil); !errors.Is(err, http.ErrUseLastResponse) { + t.Errorf("redirect error = %v, want %v", err, http.ErrUseLastResponse) + } +} + +func TestClientCanBeReusedConcurrently(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + _, _ = io.WriteString(w, `{"success":true}`) + })) + defer server.Close() + verifier := newClient("secret", server.URL, server.Client()) + var group sync.WaitGroup + for range 32 { + group.Add(1) + go func() { + defer group.Done() + result, err := verifier.Verify("token") + if err != nil || result["success"] != true { + t.Errorf("Verify() = %#v, %v", result, err) + } + }() + } + group.Wait() + if got := calls.Load(); got != 32 { + t.Errorf("calls = %d, want 32", got) + } +} + +func TestVerifyContextCancellationAndDeadline(t *testing.T) { + for _, test := range []struct { + name string + context func(context.Context) (context.Context, context.CancelFunc) + want error + }{ + {"cancellation", context.WithCancel, context.Canceled}, + {"deadline", func(ctx context.Context) (context.Context, context.CancelFunc) { + return context.WithTimeout(ctx, 50*time.Millisecond) + }, context.DeadlineExceeded}, + } { + t.Run(test.name, func(t *testing.T) { + started := make(chan struct{}) + release := make(chan struct{}) + server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + close(started) + <-release + })) + defer server.Close() + ctx, cancel := test.context(t.Context()) + defer cancel() + errs := make(chan error, 1) + go func() { + _, err := newClient("secret", server.URL, server.Client()).VerifyContext(ctx, "token") + errs <- err + }() + <-started + if errors.Is(test.want, context.Canceled) { + cancel() + } + err := <-errs + close(release) + if !errors.Is(err, test.want) { + t.Errorf("error = %v, want %v", err, test.want) + } + }) + } +} + +func TestVerifyReturnsJSONAndTransportErrors(t *testing.T) { + t.Run("request", func(t *testing.T) { + if _, err := newClient("secret", "://invalid", http.DefaultClient).Verify("token"); err == nil { + t.Fatal("Verify() error = nil, want request error") + } + }) + t.Run("json", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, `{not-json}`) })) + defer server.Close() + if _, err := newClient("secret", server.URL, server.Client()).Verify("token"); err == nil { + t.Fatal("Verify() error = nil, want JSON error") + } + }) + t.Run("transport", func(t *testing.T) { + client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, errors.New("network unavailable") }), Timeout: time.Second} + if _, err := newClient("secret", "https://example.test", client).Verify("token"); err == nil { + t.Fatal("Verify() error = nil, want transport error") + } + }) +} + +func formServer(t *testing.T, want url.Values) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("method = %q, want POST", r.Method) + http.Error(w, "wrong method", http.StatusMethodNotAllowed) + return + } + if err := r.ParseForm(); err != nil { + t.Errorf("parse form: %v", err) + http.Error(w, "invalid form", http.StatusBadRequest) + return + } + if !reflect.DeepEqual(r.PostForm, want) { + t.Errorf("form = %#v, want %#v", r.PostForm, want) + http.Error(w, "wrong form", http.StatusBadRequest) + return + } + _, _ = io.WriteString(w, `{"success":true}`) + })) +} + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (function roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return function(request) +} diff --git a/hcaptcha/integration_test.go b/hcaptcha/integration_test.go new file mode 100644 index 0000000..4320653 --- /dev/null +++ b/hcaptcha/integration_test.go @@ -0,0 +1,23 @@ +package hcaptcha + +import ( + "os" + "testing" +) + +func TestLiveSiteverify(t *testing.T) { + if os.Getenv("HCAPTCHA_LIVE_TEST") != "1" { + t.Skip("set HCAPTCHA_LIVE_TEST=1 to call the real Siteverify endpoint") + } + verifier, err := New("0x0000000000000000000000000000000000000000") + if err != nil { + t.Fatal(err) + } + result, err := verifier.Verify("10000000-aaaa-bbbb-cccc-000000000001") + if err != nil { + t.Fatal(err) + } + if result["success"] != true { + t.Fatalf("Siteverify rejected documented public test token: %+v", result) + } +}