├── .dist.gitignore ├── .envrc ├── .github └── workflows │ ├── build-dist.yaml │ ├── build.yaml │ └── lint.yaml ├── .gitignore ├── .golangci.yaml ├── .vscode └── settings.json ├── Caddyfile ├── LICENSE.anubis.md ├── LICENSE.md ├── README.md ├── cerberus.go ├── core ├── config.go ├── config_test.go ├── const.go ├── instance.go ├── pool.go ├── state.go └── state_test.go ├── devenv.lock ├── devenv.nix ├── devenv.yaml ├── directives ├── app.go ├── caddyfile.go ├── common.go ├── endpoint.go ├── i18n.go └── middleware.go ├── go.mod ├── go.sum ├── internal ├── expiremap │ └── expiremap.go ├── ipblock │ ├── ipblock.go │ └── ipblock_test.go └── randpool │ └── randpool.go ├── pow ├── .gitignore ├── Cargo.toml └── src │ ├── check_dubit.rs │ ├── lib.rs │ └── utils.rs ├── translations ├── en.yaml └── zh.yaml └── web ├── embed.go ├── global.css ├── img ├── mascot-fail.png ├── mascot-pass.png └── mascot-puzzle.png ├── index.templ └── js ├── .gitignore ├── convert.js ├── main.mjs ├── package.json ├── pnpm-lock.yaml ├── pow.mjs └── pow.worker.js /.dist.gitignore: -------------------------------------------------------------------------------- 1 | # Devenv 2 | .devenv* 3 | devenv.local.nix 4 | 5 | # direnv 6 | .direnv 7 | 8 | # pre-commit 9 | .pre-commit-config.yaml 10 | 11 | .idea/ 12 | 13 | 14 | # If you prefer the allow list template instead of the deny list, see community template: 15 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 16 | # 17 | # Binaries for programs and plugins 18 | *.exe 19 | *.exe~ 20 | *.dll 21 | *.so 22 | *.dylib 23 | 24 | # Test binary, built with `go test -c` 25 | *.test 26 | 27 | # Output of the go coverage tool, specifically when used with LiteIDE 28 | *.out 29 | 30 | # Dependency directories (remove the comment below to include it) 31 | # vendor/ 32 | 33 | # Go workspace file 34 | go.work 35 | go.work.sum 36 | 37 | # env file 38 | .env 39 | caddy 40 | 41 | .parcel-cache -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export DIRENV_WARN_TIMEOUT=20s 2 | 3 | eval "$(devenv direnvrc)" 4 | 5 | use devenv 6 | -------------------------------------------------------------------------------- /.github/workflows/build-dist.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Update Dist Branch 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version to tag (e.g., v1.0.0)' 10 | required: false 11 | default: '' 12 | 13 | jobs: 14 | build-and-deploy: 15 | name: Build artifacts and update dist branch 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout master 19 | uses: actions/checkout@v4 20 | with: 21 | ssh-key: "${{ secrets.DIST_PUSH_KEY }}" 22 | fetch-depth: 0 23 | 24 | - name: Verify version 25 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.version != '' }} 26 | run: | 27 | VERSION="${{ github.event.inputs.version }}" 28 | CODE_VERSION=$(grep -o 'Version\s*=\s*"v[^"]*"' core/const.go | cut -d'"' -f2) 29 | 30 | if [ "$VERSION" != "$CODE_VERSION" ]; then 31 | echo "::error::Version mismatch: Requested tag '$VERSION' does not match version in code '$CODE_VERSION'" 32 | echo "::error::Please update the version in core/const.go before tagging" 33 | exit 1 34 | fi 35 | 36 | echo "Version verified: $VERSION matches code version" 37 | 38 | - name: Setup Nix 39 | uses: cachix/install-nix-action@v26 40 | 41 | - name: Cache Nix 42 | uses: cachix/cachix-action@v14 43 | with: 44 | name: devenv 45 | 46 | - name: Install devenv.sh 47 | run: nix profile install nixpkgs#devenv 48 | 49 | - name: Setup Git 50 | run: | 51 | git config user.name "github-actions[bot]" 52 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 53 | 54 | - name: Create or checkout dist branch 55 | run: | 56 | COMMIT_SHA="${GITHUB_SHA}" 57 | 58 | # Check if dist branch exists 59 | if git show-ref --verify --quiet refs/remotes/origin/dist; then 60 | # If it exists, create a local copy 61 | git checkout -b dist origin/dist 62 | else 63 | # If it doesn't exist, create a new one 64 | git checkout --orphan dist 65 | git rm -rf . 66 | fi 67 | 68 | # Copy content from current ref 69 | git checkout $COMMIT_SHA -- . 70 | 71 | # Remove the original .gitignore and create a minimal one without excluding dist files 72 | rm -f .gitignore 73 | cp .dist.gitignore .gitignore 74 | 75 | - name: Build artifacts 76 | run: | 77 | devenv tasks run dist:clean 78 | devenv tasks run dist:build 79 | 80 | - name: Commit and push dist branch 81 | run: | 82 | COMMIT_SHA="${GITHUB_SHA}" 83 | 84 | # Add all files 85 | git add -A 86 | 87 | # Commit with source commit hash for reference 88 | git commit -m "Update dist branch from master (${COMMIT_SHA})" || echo "No changes to commit" 89 | 90 | # Push to dist branch 91 | git push origin dist 92 | 93 | - name: Tag dist branch 94 | if: ${{ github.event_name == 'workflow_dispatch' && inputs.version != '' }} 95 | run: | 96 | # Check if version is provided 97 | VERSION="${{ github.event.inputs.version }}" 98 | if [ -n "$VERSION" ]; then 99 | git tag -a "$VERSION" -m "Release $VERSION" 100 | git push origin "$VERSION" 101 | echo "Tagged dist branch with $VERSION" 102 | fi -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: cachix/install-nix-action@v26 15 | - uses: cachix/cachix-action@v14 16 | with: 17 | name: devenv 18 | - name: Install devenv.sh 19 | run: nix profile install nixpkgs#devenv 20 | - name: Build Web Assets 21 | run: | 22 | devenv tasks run dist:clean 23 | devenv tasks run dist:build 24 | - name: Build 25 | run: devenv shell xcaddy build 26 | - name: Upload artifacts 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: "caddy" 30 | path: "caddy" 31 | test: 32 | name: Test 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: cachix/install-nix-action@v26 37 | - uses: cachix/cachix-action@v14 38 | with: 39 | name: devenv 40 | - name: Install devenv.sh 41 | run: nix profile install nixpkgs#devenv 42 | - name: Build Web Assets 43 | run: | 44 | devenv tasks run dist:clean 45 | devenv tasks run dist:build 46 | - name: Test 47 | run: devenv test 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: cachix/install-nix-action@v26 18 | - uses: cachix/cachix-action@v14 19 | with: 20 | name: devenv 21 | - name: Install devenv.sh 22 | run: nix profile install nixpkgs#devenv 23 | - name: Build Web Assets 24 | run: | 25 | devenv tasks run dist:clean 26 | devenv tasks run dist:build 27 | - name: Lint 28 | run: devenv tasks run go:lint -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Devenv 2 | .devenv* 3 | devenv.local.nix 4 | 5 | # direnv 6 | .direnv 7 | 8 | # pre-commit 9 | .pre-commit-config.yaml 10 | 11 | .idea/ 12 | 13 | 14 | # If you prefer the allow list template instead of the deny list, see community template: 15 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 16 | # 17 | # Binaries for programs and plugins 18 | *.exe 19 | *.exe~ 20 | *.dll 21 | *.so 22 | *.dylib 23 | 24 | # Test binary, built with `go test -c` 25 | *.test 26 | 27 | # Output of the go coverage tool, specifically when used with LiteIDE 28 | *.out 29 | 30 | # Dependency directories (remove the comment below to include it) 31 | # vendor/ 32 | 33 | # Go workspace file 34 | go.work 35 | go.work.sum 36 | 37 | # env file 38 | .env 39 | caddy 40 | 41 | .parcel-cache 42 | 43 | # Generated files (excluded from master branch, but included in dist branch) 44 | web/dist 45 | web/js/icu/compiled.mjs 46 | # Note that we ignore go generated files deliberately, as downstream projects are expected to use the dist branch. 47 | web/index_templ.go -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: standard 4 | enable: 5 | - gosec 6 | - revive 7 | exclusions: 8 | generated: lax 9 | presets: 10 | - comments 11 | - common-false-positives 12 | - legacy 13 | - std-error-handling 14 | paths: 15 | - third_party$ 16 | - builtin$ 17 | - examples$ 18 | formatters: 19 | exclusions: 20 | generated: lax 21 | paths: 22 | - third_party$ 23 | - builtin$ 24 | - examples$ 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint", 3 | "go.lintFlags": [ 4 | "--fast" 5 | ] 6 | } -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | log default { 3 | level DEBUG 4 | } 5 | 6 | # Global configuration for cerberus. 7 | cerberus { 8 | # Challenge difficulty (number of leading zeroes in the hash). 9 | difficulty 12 10 | # When set to true, the handler will drop the connection instead of returning a 403 if the IP is blocked. 11 | # drop 12 | # Ed25519 signing key file path. If not provided, a new key will be generated. 13 | # ed25519_key_file "ed25519.key" 14 | # MaxPending is the maximum number of pending (and failed) requests. 15 | # Any IP block (prefix configured in prefix_cfg) with more than this number of pending requests will be blocked. 16 | max_pending 128 17 | # AccessPerApproval is the number of requests allowed per successful challenge. We recommend a value greater than 8 to support parallel and resumable downloads. 18 | access_per_approval 8 19 | # BlockTTL is the time to live for blocked IPs. 20 | block_ttl "24h" 21 | # PendingTTL is the time to live for pending requests when considering whether to block an IP. 22 | pending_ttl "1h" 23 | # ApprovalTTL is the time to live for approved requests. 24 | approval_ttl "1h" 25 | # MaxMemUsage is the maximum memory usage for the pending and blocklist caches. 26 | max_mem_usage "512MiB" 27 | # CookieName is the name of the cookie used to store signed certificate. 28 | cookie_name "cerberus-auth" 29 | # HeaderName is the name of the header used to store cerberus status ("PASS-BRIEF", "PASS-FULL", "BLOCK", "FAIL"). 30 | header_name "X-Cerberus-Status" 31 | # Title is the title of the challenge page. 32 | title "Cerberus Challenge" 33 | # Mail is the email address to contact for support. 34 | mail "admin@example.com" 35 | # PrefixCfg is to configure prefixes used to block users in these IP prefix blocks, e.g., /24 /64. 36 | # The first argument is for IPv4 and the second is for IPv6. 37 | prefix_cfg 20 64 38 | } 39 | } 40 | 41 | localhost { 42 | encode 43 | 44 | # You need to deploy a handler for each cerberus instance. 45 | # This route will be used to serve challenge endpoints and static files. 46 | handle_path /.cerberus/* { 47 | cerberus_endpoint 48 | } 49 | 50 | @cerberus { 51 | path *.iso 52 | header User-Agent *Mozilla* 53 | } 54 | 55 | # This is the actual middleware that will be used to challenge requests. 56 | # You can attach a named matcher to the cerberus directive. Only requests matching the matcher will be challenged. 57 | cerberus @cerberus { 58 | # The base URL for the challenge. It must be the same as the deployed endpoint route. 59 | base_url "/.cerberus" 60 | } 61 | 62 | @except_cerberus_endpoint { 63 | not path /.cerberus/* 64 | } 65 | 66 | # Block bad IPs except for the cerberus endpoint. 67 | cerberus @except_cerberus_endpoint { 68 | base_url "/.cerberus" 69 | # Cerberus in block only mode doesn't perform any challenge. It only blocks known bad IPs. 70 | block_only 71 | } 72 | 73 | handle / { 74 | respond "Hello, world!" 75 | } 76 | 77 | handle /foo { 78 | respond "Hello, foo!" 79 | } 80 | 81 | handle /foo.iso { 82 | respond "Hello, foo.iso!" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /LICENSE.anubis.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Xe Iaso 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Yanning Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | --- 22 | 23 | This project includes code from [Anubis](https://github.com/TecharoHQ/anubis/) licensed under the MIT License: see [LICENSE.anubis.md](LICENSE.anubis.md). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cerberus 2 | 3 |
4 | A smiling chibi dark-skinned anthro jackal with brown hair and tall ears looking victorious with a thumbs-up 5 |
6 | 7 | Cerberus guards the gates of open source infrastructure using a sha256 PoW challenge to protect them from unwanted traffic. It provides a Caddy handler that can be applied to existing Caddy servers. 8 | 9 | This project started as a Caddy port of [Anubis](https://github.com/TecharoHQ/anubis/) and is now a standalone project. While Anubis focuses on protecting websites from AI scrapers, Cerberus serves a different purpose: it's designed as a last line of defense to protect volunteer-run open source infrastructure from abusive traffic. We would do whatever it takes to stop them, even if it means sacrificing a few innocent cats. 10 | 11 | For now, the project is still mostly a re-implementation of Anubis, but it's actively developed, and will eventually employ more aggressive techniques. You can check the [Roadmap](#roadmap) section for more details. 12 | 13 | ## Usage 14 | 15 | ### Official Pre-built Binaries 16 | 17 | > Sometimes the official binaries are not up to date. In that case please build from source. 18 | 19 | 1. Install Caddy with the plugin: 20 | ```bash 21 | caddy add-package github.com/sjtug/cerberus 22 | ``` 23 | 2. Add the handler directive to your Caddyfile. Refer to the [Caddyfile](Caddyfile) for an example configuration. 24 | 25 | ### Build from Source 26 | 27 | Please build against the **dist** branch or a release tag: 28 | 29 | ```bash 30 | # Build with a specific version 31 | xcaddy build --with github.com/sjtug/cerberus@v1.0.0 32 | 33 | # Or build with the latest dist branch 34 | xcaddy build --with github.com/sjtug/cerberus@dist 35 | ``` 36 | 37 | ## Comparison with Anubis 38 | 39 | - Anubis is a standalone server that can be used with any web server, while Cerberus is a Caddy plugin. 40 | - No builtin anti-AI rules: use caddy matchers instead. 41 | - Highly aggressive challenge policy: users need to solve a challenge for every few requests and new challenges are generated per request. For further details, see the [Aggressive challenge policy](#aggressive-challenge-policy) section. 42 | - Can be set up to block IP subnets if there are too many failed challenge attempts to prevent abuse. 43 | - ~~No custom UI or anime girls.~~ Now with an AI-generated placeholder mascot lol 44 | 45 | ## Configuration 46 | 47 | Check [Caddyfile](Caddyfile) for an example configuration. 48 | 49 | ## Roadmap 50 | 51 | - [x] More frequent challenges (each solution only grants a few accesses) 52 | - [x] More frequent challenge rotation (per week -> per request) 53 | - [ ] Configurable challenge difficulty for each route 54 | - [x] "block_only" mode to serve as a blocklist even a route is not protected by PoW challenge 55 | - [x] ~~RandomX PoW~~ unacceptably slow. Use blake3 (wasm) instead. 56 | - [x] I18n 57 | - [ ] Non-AI mascot 58 | 59 | ## Aggressive challenge policy 60 | 61 | This is the first divergence from Anubis. Now, we require a user to repeat the challenge every few accesses. This is to ensure that we waste an attacker's computational resources to the extent that it becomes non-sustainable for the attacker to perform the attack. 62 | 63 | This will surely slow down legitimate users, but we believe that this is a necessary evil to protect our infrastructure. After all, a slow down is better than a complete outage. 64 | 65 | ## Development 66 | 67 | You need to first generate necessary go files before developing: 68 | ```bash 69 | $ devenv tasks run go:codegen 70 | ``` 71 | 72 | If you modified any web asset, you need to run the following command to build the dist files: 73 | ```bash 74 | $ devenv tasks run dist:build 75 | ``` 76 | 77 | Please run tests and lints before submitting a PR: 78 | ```bash 79 | $ direnv test # or go test 80 | $ devenv tasks run go:lint 81 | ``` 82 | 83 | ## Build Pipeline 84 | 85 | This repository uses a two-branch strategy: 86 | 87 | - **master branch**: Contains source code only (no generated artifacts) 88 | - **dist branch**: Contains both source code and all generated artifacts 89 | 90 | ### Release Process 91 | 92 | To create a release: 93 | 94 | 1. Update the `Version` constant in `core/const.go`. 95 | 2. Go to "Actions" → "Build and Update Dist Branch" → "Run workflow". 96 | 3. Enter the version tag (e.g., "v1.0.0") and run the workflow. -------------------------------------------------------------------------------- /cerberus.go: -------------------------------------------------------------------------------- 1 | package cerberus 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/caddyserver/caddy/v2" 7 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 8 | "github.com/sjtug/cerberus/directives" 9 | ) 10 | 11 | //go:embed translations 12 | var translations embed.FS 13 | 14 | func init() { 15 | directives.LoadI18n(translations) 16 | 17 | caddy.RegisterModule(directives.App{}) 18 | caddy.RegisterModule(directives.Middleware{}) 19 | caddy.RegisterModule(directives.Endpoint{}) 20 | httpcaddyfile.RegisterGlobalOption("cerberus", directives.ParseCaddyFileApp) 21 | httpcaddyfile.RegisterHandlerDirective("cerberus", directives.ParseCaddyFileMiddleware) 22 | httpcaddyfile.RegisterHandlerDirective("cerberus_endpoint", directives.ParseCaddyFileEndpoint) 23 | httpcaddyfile.RegisterDirectiveOrder("cerberus", httpcaddyfile.Before, "invoke") 24 | httpcaddyfile.RegisterDirectiveOrder("cerberus_endpoint", httpcaddyfile.Before, "invoke") 25 | } 26 | -------------------------------------------------------------------------------- /core/config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "time" 11 | 12 | "github.com/sjtug/cerberus/internal/ipblock" 13 | "go.uber.org/zap" 14 | "golang.org/x/crypto/ssh" 15 | ) 16 | 17 | const ( 18 | DefaultCookieName = "cerberus-auth" 19 | DefaultHeaderName = "X-Cerberus-Status" 20 | DefaultDifficulty = 4 21 | DefaultMaxPending = 128 22 | DefaultAccessPerApproval = 8 23 | DefaultBlockTTL = time.Hour * 24 // 1 day 24 | DefaultPendingTTL = time.Hour // 1 hour 25 | DefaultApprovalTTL = time.Hour // 1 hour 26 | DefaultMaxMemUsage = 1 << 29 // 512MB 27 | DefaultTitle = "Cerberus Challenge" 28 | DefaultDescription = "Making sure you're not a bot!" 29 | DefaultIPV4Prefix = 32 30 | DefaultIPV6Prefix = 64 31 | ) 32 | 33 | type Config struct { 34 | // Challenge difficulty (number of leading zeroes in the hash). 35 | Difficulty int `json:"difficulty,omitempty"` 36 | // When set to true, the handler will drop the connection instead of returning a 403 if the IP is blocked. 37 | Drop bool `json:"drop,omitempty"` 38 | // Ed25519 signing key file path. If not provided, a new key will be generated. 39 | Ed25519KeyFile string `json:"ed25519_key_file,omitempty"` 40 | // Ed25519 signing key content. If not provided, a new key will be generated. 41 | Ed25519Key string `json:"ed25519_key,omitempty"` 42 | // MaxPending is the maximum number of pending (and failed) requests. 43 | // Any IP block (prefix configured in prefix_cfg) with more than this number of pending requests will be blocked. 44 | MaxPending int32 `json:"max_pending,omitempty"` 45 | // AccessPerApproval is the number of requests allowed per successful challenge. 46 | AccessPerApproval int32 `json:"access_per_approval,omitempty"` 47 | // BlockTTL is the time to live for blocked IPs. 48 | BlockTTL time.Duration `json:"block_ttl,omitempty"` 49 | // PendingTTL is the time to live for pending requests when considering whether to block an IP. 50 | PendingTTL time.Duration `json:"pending_ttl,omitempty"` 51 | // ApprovalTTL is the time to live for approved requests. 52 | ApprovalTTL time.Duration `json:"approval_ttl,omitempty"` 53 | // MaxMemUsage is the maximum memory usage for the pending and blocklist caches. 54 | MaxMemUsage int64 `json:"max_mem_usage,omitempty"` 55 | // CookieName is the name of the cookie used to store signed certificate. 56 | CookieName string `json:"cookie_name,omitempty"` 57 | // HeaderName is the name of the header used to store cerberus status ("PASS", "CHALLENGE", "FAIL", "BLOCKED", "DISABLED"). 58 | HeaderName string `json:"header_name,omitempty"` 59 | // Title is the title of the challenge page. 60 | Title string `json:"title,omitempty"` 61 | // Mail is the email address to contact for support. 62 | Mail string `json:"mail,omitempty"` 63 | // PrefixCfg is to configure prefixes used to block users in these IP prefix blocks, e.g., /24 /64. 64 | PrefixCfg ipblock.Config `json:"prefix_cfg,omitempty"` 65 | 66 | ed25519Key ed25519.PrivateKey 67 | ed25519Pub ed25519.PublicKey 68 | } 69 | 70 | func (c *Config) Provision(logger *zap.Logger) error { 71 | if c.Difficulty == 0 { 72 | c.Difficulty = DefaultDifficulty 73 | } 74 | if c.MaxPending == 0 { 75 | c.MaxPending = DefaultMaxPending 76 | } 77 | if c.AccessPerApproval == 0 { 78 | c.AccessPerApproval = DefaultAccessPerApproval 79 | } 80 | if c.BlockTTL == time.Duration(0) { 81 | c.BlockTTL = DefaultBlockTTL 82 | } 83 | if c.PendingTTL == time.Duration(0) { 84 | c.PendingTTL = DefaultPendingTTL 85 | } 86 | if c.ApprovalTTL == time.Duration(0) { 87 | c.ApprovalTTL = DefaultApprovalTTL 88 | } 89 | if c.MaxMemUsage == 0 { 90 | c.MaxMemUsage = DefaultMaxMemUsage 91 | } 92 | if c.CookieName == "" { 93 | c.CookieName = DefaultCookieName 94 | } 95 | if c.HeaderName == "" { 96 | c.HeaderName = DefaultHeaderName 97 | } 98 | if c.Title == "" { 99 | c.Title = DefaultTitle 100 | } 101 | if c.PrefixCfg.IsEmpty() { 102 | c.PrefixCfg = ipblock.Config{ 103 | V4Prefix: DefaultIPV4Prefix, 104 | V6Prefix: DefaultIPV6Prefix, 105 | } 106 | } 107 | 108 | if c.Ed25519KeyFile != "" || c.Ed25519Key != "" { 109 | var raw []byte 110 | var err error 111 | if c.Ed25519KeyFile != "" { 112 | logger.Info("loading ed25519 key from file", zap.String("path", c.Ed25519KeyFile)) 113 | 114 | raw, err = os.ReadFile(c.Ed25519KeyFile) 115 | if err != nil { 116 | return fmt.Errorf("failed to read ed25519 key file: %w", err) 117 | } 118 | } else { 119 | raw = []byte(c.Ed25519Key) 120 | } 121 | 122 | c.ed25519Key, err = LoadEd25519Key(raw) 123 | if err != nil { 124 | return fmt.Errorf("failed to load ed25519 key: %w", err) 125 | } 126 | 127 | c.ed25519Pub = c.ed25519Key.Public().(ed25519.PublicKey) 128 | } else { 129 | logger.Info("generating new ed25519 key") 130 | var err error 131 | c.ed25519Pub, c.ed25519Key, err = ed25519.GenerateKey(nil) 132 | if err != nil { 133 | return fmt.Errorf("failed to generate ed25519 key: %w", err) 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (c *Config) Validate() error { 141 | if c.Difficulty < 1 { 142 | return errors.New("difficulty must be at least 1") 143 | } 144 | if c.MaxPending < 1 { 145 | return errors.New("max_pending must be at least 1") 146 | } 147 | if c.AccessPerApproval < 1 { 148 | return errors.New("access_per_approval must be at least 1") 149 | } 150 | if c.BlockTTL < 0 { 151 | return errors.New("block_ttl must be a positive duration") 152 | } 153 | if c.PendingTTL < 0 { 154 | return errors.New("pending_ttl must be a positive duration") 155 | } 156 | if c.ApprovalTTL < 0 { 157 | return errors.New("approval_ttl must be a positive duration") 158 | } 159 | if c.MaxMemUsage < 1 { 160 | return errors.New("max_mem_usage must be at least 1") 161 | } 162 | if c.Ed25519KeyFile != "" && c.Ed25519Key != "" { 163 | return errors.New("ed25519_key_file and ed25519_key cannot both be set") 164 | } 165 | if err := ipblock.ValidateConfig(c.PrefixCfg); err != nil { 166 | return fmt.Errorf("prefix_cfg: %w", err) 167 | } 168 | 169 | return nil 170 | } 171 | 172 | func (c *Config) StateCompatible(other *Config) bool { 173 | return c.BlockTTL == other.BlockTTL && 174 | c.PendingTTL == other.PendingTTL && 175 | c.ApprovalTTL == other.ApprovalTTL && 176 | c.AccessPerApproval == other.AccessPerApproval && 177 | c.MaxMemUsage == other.MaxMemUsage && 178 | c.PrefixCfg == other.PrefixCfg 179 | } 180 | 181 | func (c *Config) GetPublicKey() ed25519.PublicKey { 182 | return c.ed25519Pub 183 | } 184 | 185 | func (c *Config) GetPrivateKey() ed25519.PrivateKey { 186 | return c.ed25519Key 187 | } 188 | 189 | func LoadEd25519Key(data []byte) (ed25519.PrivateKey, error) { 190 | // First try to parse as openssh or x509 private key 191 | if bytes.HasPrefix(data, []byte("-----BEGIN ")) { 192 | raw, err := ssh.ParseRawPrivateKey(data) 193 | if err != nil { 194 | return nil, fmt.Errorf("failed to parse pem private key: %w", err) 195 | } 196 | if key, ok := raw.(ed25519.PrivateKey); ok { 197 | return key, nil 198 | } 199 | if key, ok := raw.(*ed25519.PrivateKey); ok { 200 | return *key, nil 201 | } 202 | return nil, errors.New("must be ed25519 private key") 203 | } 204 | 205 | // Then try to parse as hex 206 | raw, err := hex.DecodeString(string(data)) 207 | if err != nil { 208 | return nil, fmt.Errorf("failed to parse hex private key: %w", err) 209 | } 210 | if len(raw) != ed25519.SeedSize { 211 | return nil, fmt.Errorf("invalid ed25519 private key: expected %d bytes, got %d", ed25519.SeedSize, len(raw)) 212 | } 213 | 214 | key := ed25519.NewKeyFromSeed(raw) 215 | return key, nil 216 | } 217 | -------------------------------------------------------------------------------- /core/config_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLoadEd25519Key(t *testing.T) { 8 | pkcs8Key := `-----BEGIN PRIVATE KEY----- 9 | MC4CAQAwBQYDK2VwBCIEILtvnJUD4PNgkbo5um6XyBEtILLW+G4hlDoDlRNge55z 10 | -----END PRIVATE KEY-----` 11 | openSSHKey := `-----BEGIN OPENSSH PRIVATE KEY----- 12 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz 13 | c2gtZWQyNTUxOQAAACAZgIQwnZMejmZI1lXlQDTtvT4HXlA5k0nsigfNe52B8gAA 14 | AIgAAAAAAAAAAAAAAAtzc2gtZWQyNTUxOQAAACAZgIQwnZMejmZI1lXlQDTtvT4H 15 | XlA5k0nsigfNe52B8gAAAEC7b5yVA+DzYJG6Obpul8gRLSCy1vhuIZQ6A5UTYHue 16 | cxmAhDCdkx6OZkjWVeVANO29PgdeUDmTSeyKB817nYHyAAAAAAECAwQF 17 | -----END OPENSSH PRIVATE KEY-----` 18 | hexKey := "bb6f9c9503e0f36091ba39ba6e97c8112d20b2d6f86e21943a039513607b9e73" 19 | 20 | t.Run("Parse OpenSSL/PKCS8/Raw format", func(t *testing.T) { 21 | fromPKCS8, err := LoadEd25519Key([]byte(pkcs8Key)) 22 | if err != nil { 23 | t.Fatalf("failed to parse pkcs8 key: %v", err) 24 | } 25 | fromOpenSSH, err := LoadEd25519Key([]byte(openSSHKey)) 26 | if err != nil { 27 | t.Fatalf("failed to parse openssh key: %v", err) 28 | } 29 | fromHex, err := LoadEd25519Key([]byte(hexKey)) 30 | if err != nil { 31 | t.Fatalf("failed to parse hex key: %v", err) 32 | } 33 | 34 | if !fromPKCS8.Equal(fromOpenSSH) { 35 | t.Fatalf("parsed keys are not equal (pkcs8 != openssh)") 36 | } 37 | if !fromPKCS8.Equal(fromHex) { 38 | t.Fatalf("parsed keys are not equal (pkcs8 != hex)") 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /core/const.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "time" 4 | 5 | const ( 6 | AppName = "cerberus" 7 | VarIPBlock = "cerberus-block" 8 | VarReqID = "cerberus-request-id" 9 | Version = "v0.4.3" 10 | NonceTTL = 2 * time.Minute 11 | ) 12 | -------------------------------------------------------------------------------- /core/instance.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | // Instance is the shared core of the cerberus module. 8 | // There's only one instance of this struct in the entire Caddy runtime. 9 | type Instance struct { 10 | *InstanceState 11 | Config 12 | } 13 | 14 | // UpdateWithConfig updates the instance with a new config. 15 | // If the config is incompatible with the current config, its internal state will be reset. 16 | // User can pass in an optional logger to log basic metrics about the initialized state. 17 | func (i *Instance) UpdateWithConfig(c Config, logger *zap.Logger) error { 18 | logger.Info("updating cerberus instance config") 19 | if i.StateCompatible(&c) { 20 | // We only need to update the config. 21 | i.Config = c 22 | } else { 23 | // We need to reset the state. 24 | logger.Info("existing cerberus instance with incompatible config found, resetting state") 25 | state, pendingElems, blocklistElems, approvalElems, err := NewInstanceState(c) 26 | if err != nil { 27 | return err 28 | } 29 | i.Config = c 30 | i.Close() // Close the old state 31 | i.InstanceState = state 32 | logger.Info("cerberus state initialized", 33 | zap.Int64("pending_elems", pendingElems), 34 | zap.Int64("blocklist_elems", blocklistElems), 35 | zap.Int64("approval_elems", approvalElems), 36 | ) 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /core/pool.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "sync" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | var ( 10 | lock sync.RWMutex 11 | instance *Instance 12 | ) 13 | 14 | // GetInstance returns an instance of given config. 15 | // If there already exists an instance (during server reload), it will be updated with the new config. 16 | // Otherwise, a new instance will be created. 17 | // User can pass in an optional logger to log basic metrics about the initialized state. 18 | func GetInstance(config Config, logger *zap.Logger) (*Instance, error) { 19 | lock.Lock() 20 | defer lock.Unlock() 21 | 22 | if instance == nil { 23 | // Initialize a new instance. 24 | state, pendingElems, blocklistElems, approvalElems, err := NewInstanceState(config) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | logger.Info("cerberus state initialized", 30 | zap.Int64("pending_elems", pendingElems), 31 | zap.Int64("blocklist_elems", blocklistElems), 32 | zap.Int64("approval_elems", approvalElems), 33 | ) 34 | instance = &Instance{ 35 | Config: config, 36 | InstanceState: state, 37 | } 38 | return instance, nil 39 | } 40 | 41 | // Update the existing instance with the new config. 42 | err := instance.UpdateWithConfig(config, logger) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return instance, nil 48 | } 49 | -------------------------------------------------------------------------------- /core/state.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | "unsafe" 7 | 8 | "crypto/sha256" 9 | "encoding/binary" 10 | "encoding/hex" 11 | 12 | "github.com/elastic/go-freelru" 13 | "github.com/google/uuid" 14 | "github.com/sjtug/cerberus/internal/expiremap" 15 | "github.com/sjtug/cerberus/internal/ipblock" 16 | "github.com/zeebo/xxh3" 17 | ) 18 | 19 | const ( 20 | FreeLRUInternalCost = 20 21 | PendingItemCost = FreeLRUInternalCost + int64(unsafe.Sizeof(ipblock.IPBlock{})) + int64(unsafe.Sizeof(&atomic.Int32{})) + int64(unsafe.Sizeof(atomic.Int32{})) 22 | BlocklistItemCost = FreeLRUInternalCost + int64(unsafe.Sizeof(ipblock.IPBlock{})) 23 | ApprovalItemCost = FreeLRUInternalCost + int64(unsafe.Sizeof(uuid.UUID{})) + int64(unsafe.Sizeof(&atomic.Int32{})) + int64(unsafe.Sizeof(atomic.Int32{})) 24 | ) 25 | 26 | func hashIPBlock(ip ipblock.IPBlock) uint32 { 27 | data := ip.ToUint64() 28 | 29 | var buf [8]byte 30 | binary.BigEndian.PutUint64(buf[:], data) 31 | 32 | hash := xxh3.Hash(buf[:]) 33 | return uint32(hash) // #nosec G115 -- expected truncation 34 | } 35 | 36 | func hashUUID(id uuid.UUID) uint32 { 37 | hash := xxh3.Hash(id[:]) 38 | return uint32(hash) // #nosec G115 -- expected truncation 39 | } 40 | 41 | type InstanceState struct { 42 | fp string 43 | pending freelru.Cache[ipblock.IPBlock, *atomic.Int32] 44 | blocklist freelru.Cache[ipblock.IPBlock, struct{}] 45 | approval freelru.Cache[uuid.UUID, *atomic.Int32] 46 | usedNonce *expiremap.ExpireMap[uint32, struct{}] 47 | stop chan struct{} 48 | } 49 | 50 | // initLRU creates and initializes an LRU cache with the given parameters 51 | func initLRU[K comparable, V any]( 52 | elems uint32, 53 | hashFunc func(K) uint32, 54 | ttl time.Duration, 55 | stop chan struct{}, 56 | purgeInterval time.Duration, 57 | ) (freelru.Cache[K, V], error) { 58 | cache, err := freelru.NewSharded[K, V](elems, hashFunc) 59 | if err != nil { 60 | return nil, err 61 | } 62 | cache.SetLifetime(ttl) 63 | 64 | go func() { 65 | for { 66 | select { 67 | case <-stop: 68 | return 69 | case <-time.After(purgeInterval): 70 | cache.PurgeExpired() 71 | } 72 | } 73 | }() 74 | 75 | return cache, nil 76 | } 77 | 78 | // initUsedNonce creates and initializes an ExpireMap for tracking used nonces 79 | func initUsedNonce(stop chan struct{}, purgeInterval time.Duration) *expiremap.ExpireMap[uint32, struct{}] { 80 | usedNonce := expiremap.NewExpireMap[uint32, struct{}](func(x uint32) uint32 { 81 | return x 82 | }) 83 | go func() { 84 | for { 85 | select { 86 | case <-stop: 87 | return 88 | case <-time.After(purgeInterval): 89 | usedNonce.PurgeExpired() 90 | } 91 | } 92 | }() 93 | return usedNonce 94 | } 95 | 96 | func NewInstanceState(config Config) (*InstanceState, int64, int64, int64, error) { 97 | uuid.EnableRandPool() 98 | 99 | stop := make(chan struct{}) 100 | 101 | pendingMaxMemUsage := config.MaxMemUsage / 10 102 | blocklistMaxMemUsage := config.MaxMemUsage / 10 103 | approvalMaxMemUsage := config.MaxMemUsage * 4 / 5 104 | 105 | pendingElems := uint32(pendingMaxMemUsage / PendingItemCost) // #nosec G115 we trust config input 106 | pending, err := initLRU[ipblock.IPBlock, *atomic.Int32]( 107 | pendingElems, 108 | hashIPBlock, 109 | config.PendingTTL, 110 | stop, 111 | 37*time.Second, 112 | ) 113 | if err != nil { 114 | return nil, 0, 0, 0, err 115 | } 116 | 117 | blocklistElems := uint32(blocklistMaxMemUsage / BlocklistItemCost) // #nosec G115 we trust config input 118 | blocklist, err := initLRU[ipblock.IPBlock, struct{}]( 119 | blocklistElems, 120 | hashIPBlock, 121 | config.BlockTTL, 122 | stop, 123 | 61*time.Second, 124 | ) 125 | if err != nil { 126 | return nil, 0, 0, 0, err 127 | } 128 | 129 | approvalElems := uint32(approvalMaxMemUsage / ApprovalItemCost) // #nosec G115 we trust config input 130 | approval, err := initLRU[uuid.UUID, *atomic.Int32]( 131 | approvalElems, 132 | hashUUID, 133 | config.ApprovalTTL, 134 | stop, 135 | 43*time.Second, 136 | ) 137 | if err != nil { 138 | return nil, 0, 0, 0, err 139 | } 140 | 141 | usedNonce := initUsedNonce(stop, 41*time.Second) 142 | 143 | fp := sha256.Sum256(config.ed25519Key.Seed()) 144 | 145 | return &InstanceState{ 146 | fp: hex.EncodeToString(fp[:]), 147 | pending: pending, 148 | blocklist: blocklist, 149 | approval: approval, 150 | usedNonce: usedNonce, 151 | stop: stop, 152 | }, int64(pendingElems), int64(blocklistElems), int64(approvalElems), nil 153 | } 154 | 155 | func (s *InstanceState) GetFingerprint() string { 156 | return s.fp 157 | } 158 | 159 | func (s *InstanceState) IncPending(ip ipblock.IPBlock) int32 { 160 | counter, ok := s.pending.Get(ip) 161 | if ok { 162 | return counter.Add(1) 163 | } 164 | 165 | var newCounter atomic.Int32 166 | newCounter.Store(1) 167 | s.pending.Add(ip, &newCounter) 168 | return 1 169 | } 170 | 171 | func (s *InstanceState) DecPending(ip ipblock.IPBlock) int32 { 172 | counter, ok := s.pending.Get(ip) 173 | if ok { 174 | count := counter.Add(-1) 175 | if count <= 0 { 176 | s.pending.Remove(ip) 177 | return 0 178 | } 179 | return count 180 | } 181 | 182 | return 0 183 | } 184 | 185 | func (s *InstanceState) RemovePending(ip ipblock.IPBlock) bool { 186 | return s.pending.Remove(ip) 187 | } 188 | 189 | func (s *InstanceState) InsertBlocklist(ip ipblock.IPBlock) { 190 | s.blocklist.Add(ip, struct{}{}) 191 | } 192 | 193 | func (s *InstanceState) ContainsBlocklist(ip ipblock.IPBlock) bool { 194 | _, ok := s.blocklist.Get(ip) 195 | return ok 196 | } 197 | 198 | // IssueApproval issues a new approval ID and returns it 199 | func (s *InstanceState) IssueApproval(n int32) uuid.UUID { 200 | id := uuid.New() 201 | 202 | var counter atomic.Int32 203 | counter.Store(n) 204 | 205 | s.approval.Add(id, &counter) 206 | return id 207 | } 208 | 209 | // DecApproval decrements the counter of the approval ID and returns whether the ID is still valid 210 | func (s *InstanceState) DecApproval(id uuid.UUID) bool { 211 | counter, ok := s.approval.Get(id) 212 | if ok { 213 | count := counter.Add(-1) 214 | if count < 0 { 215 | s.approval.Remove(id) 216 | return false 217 | } 218 | return true 219 | } 220 | return false 221 | } 222 | 223 | // InsertUsedNonce inserts a nonce into the usedNonce map. 224 | // Returns true if the nonce was inserted, false if it was already present. 225 | func (s *InstanceState) InsertUsedNonce(nonce uint32) bool { 226 | return s.usedNonce.SetIfAbsent(nonce, struct{}{}, NonceTTL) 227 | } 228 | 229 | func (s *InstanceState) Close() { 230 | close(s.stop) 231 | } 232 | -------------------------------------------------------------------------------- /core/state_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/sjtug/cerberus/internal/ipblock" 10 | ) 11 | 12 | func newTestState(t *testing.T) *InstanceState { 13 | pub, priv, err := ed25519.GenerateKey(nil) 14 | if err != nil { 15 | t.Fatalf("failed to generate ed25519 key: %v", err) 16 | } 17 | 18 | state, _, _, _, err := NewInstanceState(Config{ 19 | MaxMemUsage: 10 << 20, // 10MB for pending 20 | PendingTTL: time.Hour, // 1 hour TTL for pending 21 | BlockTTL: time.Hour, // 1 hour TTL for blocklist 22 | ApprovalTTL: time.Hour, // 1 hour TTL for approved 23 | ed25519Pub: pub, 24 | ed25519Key: priv, 25 | }) 26 | if err != nil { 27 | t.Fatalf("failed to create instance state: %v", err) 28 | } 29 | return state 30 | } 31 | 32 | func newTestIPBlock(t *testing.T, ipStr string) ipblock.IPBlock { 33 | ip := net.ParseIP(ipStr) 34 | ipBlock, err := ipblock.NewIPBlock(ip, ipblock.Config{V4Prefix: 24, V6Prefix: 64}) 35 | if err != nil { 36 | t.Fatalf("failed to create IP block: %v", err) 37 | } 38 | return ipBlock 39 | } 40 | 41 | func TestPending(t *testing.T) { 42 | state := newTestState(t) 43 | defer state.Close() 44 | ipBlock := newTestIPBlock(t, "192.168.1.1") 45 | 46 | tests := []struct { 47 | name string 48 | action func() int32 49 | expected int32 50 | }{ 51 | { 52 | name: "initial increment", 53 | action: func() int32 { 54 | return state.IncPending(ipBlock) 55 | }, 56 | expected: 1, 57 | }, 58 | { 59 | name: "second increment", 60 | action: func() int32 { 61 | return state.IncPending(ipBlock) 62 | }, 63 | expected: 2, 64 | }, 65 | { 66 | name: "decrement", 67 | action: func() int32 { 68 | return state.DecPending(ipBlock) 69 | }, 70 | expected: 1, 71 | }, 72 | } 73 | 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | if got := tt.action(); got != tt.expected { 77 | t.Errorf("expected count to be %d, got %d", tt.expected, got) 78 | } 79 | }) 80 | } 81 | 82 | // Test removing pending 83 | t.Run("remove pending", func(t *testing.T) { 84 | if !state.RemovePending(ipBlock) { 85 | t.Error("expected pending to be removed") 86 | } 87 | }) 88 | 89 | // Test that pending is actually removed 90 | t.Run("verify removal", func(t *testing.T) { 91 | if count := state.IncPending(ipBlock); count != 1 { 92 | t.Errorf("expected count to be 1 after removal, got %d", count) 93 | } 94 | }) 95 | } 96 | 97 | func TestPendingSubnets(t *testing.T) { 98 | state := newTestState(t) 99 | defer state.Close() 100 | 101 | // Create IP blocks in different subnets 102 | ipBlock1 := newTestIPBlock(t, "192.168.1.1") 103 | ipBlock2 := newTestIPBlock(t, "192.169.1.1") 104 | 105 | tests := []struct { 106 | name string 107 | ipBlock ipblock.IPBlock 108 | action func() int32 109 | expected int32 110 | }{ 111 | { 112 | name: "first subnet initial increment", 113 | ipBlock: ipBlock1, 114 | action: func() int32 { 115 | return state.IncPending(ipBlock1) 116 | }, 117 | expected: 1, 118 | }, 119 | { 120 | name: "second subnet initial increment", 121 | ipBlock: ipBlock2, 122 | action: func() int32 { 123 | return state.IncPending(ipBlock2) 124 | }, 125 | expected: 1, 126 | }, 127 | { 128 | name: "first subnet second increment", 129 | ipBlock: ipBlock1, 130 | action: func() int32 { 131 | return state.IncPending(ipBlock1) 132 | }, 133 | expected: 2, 134 | }, 135 | { 136 | name: "second subnet second increment", 137 | ipBlock: ipBlock2, 138 | action: func() int32 { 139 | return state.IncPending(ipBlock2) 140 | }, 141 | expected: 2, 142 | }, 143 | } 144 | 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | if got := tt.action(); got != tt.expected { 148 | t.Errorf("expected count for %s to be %d, got %d", tt.ipBlock.ToIPNet(ipblock.Config{V4Prefix: 24, V6Prefix: 64}).String(), tt.expected, got) 149 | } 150 | }) 151 | } 152 | } 153 | 154 | func TestBlocklist(t *testing.T) { 155 | state := newTestState(t) 156 | defer state.Close() 157 | 158 | ipBlock := newTestIPBlock(t, "192.168.1.1") 159 | ipBlock2 := newTestIPBlock(t, "192.168.1.2") // Same block 160 | ipBlock3 := newTestIPBlock(t, "192.169.1.1") // Different block 161 | 162 | tests := []struct { 163 | name string 164 | ipBlock ipblock.IPBlock 165 | expected bool 166 | }{ 167 | { 168 | name: "initial state", 169 | ipBlock: ipBlock, 170 | expected: false, 171 | }, 172 | { 173 | name: "same block after insertion", 174 | ipBlock: ipBlock2, 175 | expected: true, 176 | }, 177 | { 178 | name: "different block", 179 | ipBlock: ipBlock3, 180 | expected: false, 181 | }, 182 | } 183 | 184 | // Test initial state 185 | t.Run(tests[0].name, func(t *testing.T) { 186 | if state.ContainsBlocklist(tests[0].ipBlock) { 187 | t.Error("expected IP to not be in blocklist initially") 188 | } 189 | }) 190 | 191 | // Insert into blocklist 192 | state.InsertBlocklist(ipBlock) 193 | 194 | // Test remaining cases 195 | for _, tt := range tests[1:] { 196 | t.Run(tt.name, func(t *testing.T) { 197 | if got := state.ContainsBlocklist(tt.ipBlock); got != tt.expected { 198 | t.Errorf("expected blocklist status to be %v, got %v", tt.expected, got) 199 | } 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /devenv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devenv": { 4 | "locked": { 5 | "dir": "src/modules", 6 | "lastModified": 1747717470, 7 | "owner": "cachix", 8 | "repo": "devenv", 9 | "rev": "c7f2256ee4a4a4ee9cbf1e82a6e49b253c374995", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "dir": "src/modules", 14 | "owner": "cachix", 15 | "repo": "devenv", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-compat": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1747046372, 23 | "owner": "edolstra", 24 | "repo": "flake-compat", 25 | "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "edolstra", 30 | "repo": "flake-compat", 31 | "type": "github" 32 | } 33 | }, 34 | "git-hooks": { 35 | "inputs": { 36 | "flake-compat": "flake-compat", 37 | "gitignore": "gitignore", 38 | "nixpkgs": [ 39 | "nixpkgs" 40 | ] 41 | }, 42 | "locked": { 43 | "lastModified": 1747372754, 44 | "owner": "cachix", 45 | "repo": "git-hooks.nix", 46 | "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "cachix", 51 | "repo": "git-hooks.nix", 52 | "type": "github" 53 | } 54 | }, 55 | "gitignore": { 56 | "inputs": { 57 | "nixpkgs": [ 58 | "git-hooks", 59 | "nixpkgs" 60 | ] 61 | }, 62 | "locked": { 63 | "lastModified": 1709087332, 64 | "owner": "hercules-ci", 65 | "repo": "gitignore.nix", 66 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "hercules-ci", 71 | "repo": "gitignore.nix", 72 | "type": "github" 73 | } 74 | }, 75 | "nixpkgs": { 76 | "locked": { 77 | "lastModified": 1747885982, 78 | "owner": "nixos", 79 | "repo": "nixpkgs", 80 | "rev": "a16efe5d2fc7455d7328a01f4692bfec152965b3", 81 | "type": "github" 82 | }, 83 | "original": { 84 | "owner": "nixos", 85 | "ref": "nixpkgs-unstable", 86 | "repo": "nixpkgs", 87 | "type": "github" 88 | } 89 | }, 90 | "root": { 91 | "inputs": { 92 | "devenv": "devenv", 93 | "git-hooks": "git-hooks", 94 | "nixpkgs": "nixpkgs", 95 | "pre-commit-hooks": [ 96 | "git-hooks" 97 | ] 98 | } 99 | } 100 | }, 101 | "root": "root", 102 | "version": 7 103 | } 104 | -------------------------------------------------------------------------------- /devenv.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | 3 | { 4 | # https://devenv.sh/packages/ 5 | packages = with pkgs; [ 6 | git 7 | xcaddy 8 | templ 9 | esbuild 10 | golangci-lint 11 | tailwindcss_4 12 | pngquant 13 | wasm-pack 14 | ]; 15 | 16 | # https://devenv.sh/languages/ 17 | languages.go = { 18 | enable = true; 19 | enableHardeningWorkaround = true; 20 | }; 21 | 22 | tasks = 23 | let 24 | tailwindcss = "${pkgs.tailwindcss_4}/bin/tailwindcss"; 25 | find = "${pkgs.findutils}/bin/find"; 26 | xargs = "${pkgs.findutils}/bin/xargs"; 27 | pngquant = "${pkgs.pngquant}/bin/pngquant"; 28 | templ = "${pkgs.templ}/bin/templ"; 29 | wasm-pack = "${pkgs.wasm-pack}/bin/wasm-pack"; 30 | pnpm = "${pkgs.nodePackages.pnpm}/bin/pnpm"; 31 | pnpx = "${pkgs.nodePackages.pnpm}/bin/pnpx"; 32 | golangci-lint = "${pkgs.golangci-lint}/bin/golangci-lint"; 33 | node = "${pkgs.nodejs}/bin/node"; 34 | in 35 | { 36 | "css:build".exec = "${tailwindcss} -i ./web/global.css -o ./web/dist/global.css --minify"; 37 | "wasm:build".exec = '' 38 | ${wasm-pack} build --target web ./pow --no-default-features 39 | ''; 40 | "js:install" = { 41 | exec = '' 42 | cd web/js 43 | ${pnpm} install 44 | ''; 45 | after = [ "wasm:build" ]; 46 | }; 47 | "js:bundle" = { 48 | exec = '' 49 | cd web/js 50 | ${pnpx} parcel build --dist-dir ../dist/ 51 | ''; 52 | after = [ 53 | "js:install" 54 | "js:icu" 55 | ]; 56 | }; 57 | "img:dist".exec = '' 58 | mkdir -p ./web/dist/img 59 | ${find} ./web/img -maxdepth 1 -name "*.png" -printf "%f\n" | ${xargs} -n 1 sh -c '${pngquant} --force --strip --quality 0-20 --speed 1 ./web/img/$0 -o ./web/dist/img/$0' 60 | ''; 61 | "go:codegen".exec = "${templ} generate"; 62 | "js:icu" = { 63 | exec = '' 64 | cd web/js 65 | mkdir -p icu 66 | ${node} convert.js ../../translations icu/compiled.mjs 67 | ''; 68 | after = [ "js:install" ]; 69 | }; 70 | "dist:clean".exec = "rm -rf ./web/dist"; 71 | "dist:build".after = [ 72 | "css:build" 73 | "js:bundle" 74 | "img:dist" 75 | "go:codegen" 76 | ]; 77 | "go:lint" = { 78 | exec = "${golangci-lint} run"; 79 | after = [ "go:codegen" ]; 80 | }; 81 | }; 82 | 83 | # tasks = { 84 | # "myproj:setup".exec = "mytool build"; 85 | # "devenv:enterShell".after = [ "myproj:setup" ]; 86 | # }; 87 | 88 | # https://devenv.sh/tests/ 89 | enterTest = '' 90 | echo "Running tests" 91 | go test ./... 92 | ''; 93 | 94 | # https://devenv.sh/git-hooks/ 95 | # git-hooks.hooks.shellcheck.enable = true; 96 | 97 | # See full reference at https://devenv.sh/reference/options/ 98 | } 99 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://devenv.sh/devenv.schema.json 2 | inputs: 3 | nixpkgs: 4 | url: github:nixos/nixpkgs/nixpkgs-unstable 5 | 6 | # If you're using non-OSS software, you can set allowUnfree to true. 7 | # allowUnfree: true 8 | 9 | # If you're willing to use a package that's vulnerable 10 | # permittedInsecurePackages: 11 | # - "openssl-1.1.1w" 12 | 13 | # If you have more than one devenv you can merge them 14 | #imports: 15 | # - ./backend 16 | -------------------------------------------------------------------------------- /directives/app.go: -------------------------------------------------------------------------------- 1 | package directives 2 | 3 | import ( 4 | "github.com/caddyserver/caddy/v2" 5 | "github.com/sjtug/cerberus/core" 6 | ) 7 | 8 | // App is the global configuration for cerberus. 9 | // There can only be one cerberus app in the entire Caddy runtime. 10 | type App struct { 11 | core.Config 12 | instance *core.Instance 13 | } 14 | 15 | func (c *App) GetInstance() *core.Instance { 16 | return c.instance 17 | } 18 | 19 | func (c *App) Provision(context caddy.Context) error { 20 | err := c.Config.Provision(context.Logger()) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | context.Logger().Debug("cerberus instance provision") 26 | 27 | instance, err := core.GetInstance(c.Config, context.Logger()) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | c.instance = instance 33 | 34 | return nil 35 | } 36 | 37 | func (c *App) Validate() error { 38 | return c.Config.Validate() 39 | } 40 | 41 | func (c *App) Start() error { 42 | return nil 43 | } 44 | 45 | func (c *App) Stop() error { 46 | return nil 47 | } 48 | 49 | func (App) CaddyModule() caddy.ModuleInfo { 50 | return caddy.ModuleInfo{ 51 | ID: "cerberus", 52 | New: func() caddy.Module { return new(App) }, 53 | } 54 | } 55 | 56 | var ( 57 | _ caddy.App = (*App)(nil) 58 | _ caddy.Provisioner = (*App)(nil) 59 | _ caddy.Validator = (*App)(nil) 60 | ) 61 | -------------------------------------------------------------------------------- /directives/caddyfile.go: -------------------------------------------------------------------------------- 1 | package directives 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/caddyserver/caddy/v2/caddyconfig" 7 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 8 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 9 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 10 | "github.com/dustin/go-humanize" 11 | "github.com/sjtug/cerberus/core" 12 | "github.com/sjtug/cerberus/internal/ipblock" 13 | ) 14 | 15 | func (c *App) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 16 | d.Next() // consume the directive 17 | 18 | for nesting := d.Nesting(); d.NextBlock(nesting); { 19 | switch d.Val() { 20 | case "difficulty": 21 | if !d.NextArg() { 22 | return d.ArgErr() 23 | } 24 | difficulty, ok := d.ScalarVal().(int) 25 | if !ok { 26 | return d.Errf("difficulty must be an integer") 27 | } 28 | c.Difficulty = difficulty 29 | case "drop": 30 | if !d.NextArg() { 31 | c.Drop = true 32 | continue 33 | } 34 | drop, ok := d.ScalarVal().(bool) 35 | if !ok { 36 | return d.Errf("drop must be a boolean") 37 | } 38 | c.Drop = drop 39 | case "ed25519_key_file": 40 | if !d.NextArg() { 41 | return d.ArgErr() 42 | } 43 | ed25519KeyFile, ok := d.ScalarVal().(string) 44 | if !ok { 45 | return d.Errf("ed25519_key_file must be a string") 46 | } 47 | c.Ed25519KeyFile = ed25519KeyFile 48 | case "max_pending": 49 | if !d.NextArg() { 50 | return d.ArgErr() 51 | } 52 | maxPending, ok := d.ScalarVal().(int) 53 | if !ok { 54 | return d.Errf("max_pending must be an integer") 55 | } 56 | c.MaxPending = int32(maxPending) // #nosec G115 -- trusted input 57 | case "access_per_approval": 58 | if !d.NextArg() { 59 | return d.ArgErr() 60 | } 61 | accessPerApproval, ok := d.ScalarVal().(int) 62 | if !ok { 63 | return d.Errf("access_per_approval must be an integer") 64 | } 65 | c.AccessPerApproval = int32(accessPerApproval) // #nosec G115 -- trusted input 66 | case "block_ttl": 67 | if !d.NextArg() { 68 | return d.ArgErr() 69 | } 70 | blockTTLRaw, ok := d.ScalarVal().(string) 71 | if !ok { 72 | return d.Errf("block_ttl must be a string") 73 | } 74 | blockTTL, err := time.ParseDuration(blockTTLRaw) 75 | if err != nil { 76 | return d.Errf("block_ttl must be a valid duration: %v", err) 77 | } 78 | c.BlockTTL = blockTTL 79 | case "pending_ttl": 80 | if !d.NextArg() { 81 | return d.ArgErr() 82 | } 83 | pendingTTLRaw, ok := d.ScalarVal().(string) 84 | if !ok { 85 | return d.Errf("pending_ttl must be a string") 86 | } 87 | pendingTTL, err := time.ParseDuration(pendingTTLRaw) 88 | if err != nil { 89 | return d.Errf("pending_ttl must be a valid duration: %v", err) 90 | } 91 | c.PendingTTL = pendingTTL 92 | case "approval_ttl": 93 | if !d.NextArg() { 94 | return d.ArgErr() 95 | } 96 | approvalTTLRaw, ok := d.ScalarVal().(string) 97 | if !ok { 98 | return d.Errf("approval_ttl must be a string") 99 | } 100 | approvalTTL, err := time.ParseDuration(approvalTTLRaw) 101 | if err != nil { 102 | return d.Errf("approval_ttl must be a valid duration: %v", err) 103 | } 104 | c.ApprovalTTL = approvalTTL 105 | case "max_mem_usage": 106 | if !d.NextArg() { 107 | return d.ArgErr() 108 | } 109 | maxMemUsageRaw, ok := d.ScalarVal().(string) 110 | if !ok { 111 | return d.Errf("max_mem_usage must be a string") 112 | } 113 | maxMemUsage, err := humanize.ParseBytes(maxMemUsageRaw) 114 | if err != nil { 115 | return d.Errf("max_mem_usage must be a valid size: %v", err) 116 | } 117 | c.MaxMemUsage = int64(maxMemUsage) // #nosec G115 -- trusted input 118 | case "cookie_name": 119 | if !d.NextArg() { 120 | return d.ArgErr() 121 | } 122 | cookieName, ok := d.ScalarVal().(string) 123 | if !ok { 124 | return d.Errf("cookie_name must be a string") 125 | } 126 | c.CookieName = cookieName 127 | case "header_name": 128 | if !d.NextArg() { 129 | return d.ArgErr() 130 | } 131 | headerName, ok := d.ScalarVal().(string) 132 | if !ok { 133 | return d.Errf("header_name must be a string") 134 | } 135 | c.HeaderName = headerName 136 | case "title": 137 | if !d.NextArg() { 138 | return d.ArgErr() 139 | } 140 | title, ok := d.ScalarVal().(string) 141 | if !ok { 142 | return d.Errf("title must be a string") 143 | } 144 | c.Title = title 145 | case "prefix_cfg": 146 | if !d.NextArg() { 147 | return d.ArgErr() 148 | } 149 | v4Prefix, ok := d.ScalarVal().(int) 150 | if !ok { 151 | return d.Errf("prefix_cfg must be followed by two integers") 152 | } 153 | if !d.NextArg() { 154 | return d.ArgErr() 155 | } 156 | v6Prefix, ok := d.ScalarVal().(int) 157 | if !ok { 158 | return d.Errf("prefix_cfg must be followed by two integers") 159 | } 160 | c.PrefixCfg = ipblock.Config{ 161 | V4Prefix: v4Prefix, 162 | V6Prefix: v6Prefix, 163 | } 164 | case "mail": 165 | if !d.NextArg() { 166 | return d.ArgErr() 167 | } 168 | mail, ok := d.ScalarVal().(string) 169 | if !ok { 170 | return d.Errf("mail must be a string") 171 | } 172 | c.Mail = mail 173 | default: 174 | return d.Errf("unknown subdirective '%s'", d.Val()) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func ParseCaddyFileApp(d *caddyfile.Dispenser, _ any) (any, error) { 182 | var c App 183 | err := c.UnmarshalCaddyfile(d) 184 | return httpcaddyfile.App{ 185 | Name: core.AppName, 186 | Value: caddyconfig.JSON(c, nil), 187 | }, err 188 | } 189 | 190 | func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 191 | d.Next() // consume the directive 192 | 193 | for nesting := d.Nesting(); d.NextBlock(nesting); { 194 | switch d.Val() { 195 | case "base_url": 196 | if !d.NextArg() { 197 | return d.ArgErr() 198 | } 199 | baseURL, ok := d.ScalarVal().(string) 200 | if !ok { 201 | return d.Errf("base_url must be a string") 202 | } 203 | m.BaseURL = baseURL 204 | case "block_only": 205 | if !d.NextArg() { 206 | m.BlockOnly = true 207 | continue 208 | } 209 | blockOnly, ok := d.ScalarVal().(bool) 210 | if !ok { 211 | return d.Errf("block_only must be a boolean") 212 | } 213 | m.BlockOnly = blockOnly 214 | default: 215 | return d.Errf("unknown subdirective '%s'", d.Val()) 216 | } 217 | } 218 | return nil 219 | } 220 | 221 | func ParseCaddyFileMiddleware(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 222 | var m Middleware 223 | err := m.UnmarshalCaddyfile(h.Dispenser) 224 | return &m, err 225 | } 226 | 227 | func (e *Endpoint) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 228 | d.Next() // consume the directive 229 | 230 | return nil 231 | } 232 | 233 | func ParseCaddyFileEndpoint(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 234 | var e Endpoint 235 | err := e.UnmarshalCaddyfile(h.Dispenser) 236 | return &e, err 237 | } 238 | -------------------------------------------------------------------------------- /directives/common.go: -------------------------------------------------------------------------------- 1 | package directives 2 | 3 | import ( 4 | "context" 5 | "crypto/ed25519" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/a-h/templ" 13 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 14 | "github.com/golang-jwt/jwt/v5" 15 | "github.com/google/uuid" 16 | "github.com/invopop/ctxi18n" 17 | "github.com/invopop/ctxi18n/i18n" 18 | "github.com/sjtug/cerberus/core" 19 | "github.com/sjtug/cerberus/web" 20 | "github.com/zeebo/blake3" 21 | ) 22 | 23 | const ( 24 | IV1 = "/L4y6KgWa8vHEujU3O6JyI8osQxwh1nE0Eoay4nD3vw/y36eSFT0s/GTGfrngN6+" 25 | IV2 = "KHo5hHR3ZfisR7xeG1gJwO3LSc1cYyDUQ5+StoAjV8jLhp01NBNi4joHYTWXDqF0" 26 | ) 27 | 28 | func clearCookie(w http.ResponseWriter, cookieName string) { 29 | http.SetCookie(w, &http.Cookie{ 30 | Name: cookieName, 31 | Value: "", 32 | Expires: time.Now().Add(-time.Hour), 33 | MaxAge: -1, 34 | SameSite: http.SameSiteLaxMode, 35 | }) 36 | } 37 | 38 | func validateCookie(cookie *http.Cookie) error { 39 | if err := cookie.Valid(); err != nil { 40 | return err 41 | } 42 | 43 | if time.Now().After(cookie.Expires) && !cookie.Expires.IsZero() { 44 | return errors.New("cookie expired") 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func validateToken(token *jwt.Token) error { 51 | if token == nil { 52 | return fmt.Errorf("token is nil") 53 | } 54 | 55 | if !token.Valid { 56 | return fmt.Errorf("token is not valid") 57 | } 58 | 59 | claims := token.Claims.(jwt.MapClaims) 60 | 61 | exp, ok := claims["exp"].(float64) 62 | if !ok { 63 | return fmt.Errorf("token does not contain exp claim") 64 | } 65 | 66 | if exp := time.Unix(int64(exp), 0); exp.Before(time.Now()) { 67 | return fmt.Errorf("token expired at %s", exp) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func blake3sum(text string) (string, error) { 74 | hash := blake3.New() 75 | _, err := hash.WriteString(text) 76 | if err != nil { 77 | return "", err 78 | } 79 | return hex.EncodeToString(hash.Sum(nil)), nil 80 | } 81 | 82 | func challengeFor(r *http.Request, c *core.Instance) (string, error) { 83 | fp := c.GetFingerprint() 84 | 85 | payload := fmt.Sprintf("Accept-Language=%s,X-Real-IP=%s,User-Agent=%s,Fingerprint=%s,Difficulty=%d,IV=%s", 86 | r.Header.Get("Accept-Language"), 87 | getClientIP(r), 88 | r.Header.Get("User-Agent"), 89 | fp, 90 | c.Difficulty, 91 | IV1, 92 | ) 93 | 94 | return blake3sum(payload) 95 | } 96 | 97 | func calcSignature(challenge string, nonce uint32, ts int64, c *core.Instance) string { 98 | payload := fmt.Sprintf("Challenge=%s,Nonce=%d,TS=%d,IV=%s", challenge, nonce, ts, IV2) 99 | 100 | signature := ed25519.Sign(c.GetPrivateKey(), []byte(payload)) 101 | return hex.EncodeToString(signature) 102 | } 103 | 104 | func respondFailure(w http.ResponseWriter, r *http.Request, c *core.Config, msg string, blocked bool, status int, baseURL string) error { 105 | // Do not cache failure responses. 106 | w.Header().Set("Cache-Control", "no-cache") 107 | 108 | if blocked { 109 | if c.Drop { 110 | // Drop the connection 111 | panic(http.ErrAbortHandler) 112 | } 113 | w.Header().Set(c.HeaderName, "BLOCKED") 114 | // Close the connection to the client 115 | r.Close = true 116 | w.Header().Set("Connection", "close") 117 | return renderTemplate(w, r, c, baseURL, 118 | i18n.T(r.Context(), "error.access_restricted"), 119 | web.Error( 120 | i18n.T(r.Context(), "error.ip_blocked"), 121 | i18n.T(r.Context(), "error.wait_before_retry"), 122 | c.Mail, 123 | ), 124 | templ.WithStatus(status), 125 | ) 126 | } 127 | 128 | w.Header().Set(c.HeaderName, "FAIL") 129 | return renderTemplate(w, r, c, baseURL, 130 | i18n.T(r.Context(), "error.error_occurred"), 131 | web.Error( 132 | msg, 133 | i18n.T(r.Context(), "error.browser_config_or_bug"), 134 | c.Mail, 135 | ), 136 | templ.WithStatus(status), 137 | ) 138 | } 139 | 140 | func setupLocale(r *http.Request) (*http.Request, error) { 141 | locale := r.Header.Get("Accept-Language") 142 | if locale == "" { 143 | locale = "en" 144 | } 145 | 146 | ctx, err := ctxi18n.WithLocale(r.Context(), locale) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | ctx = context.WithValue(ctx, web.LocaleCtxKey, locale) 152 | 153 | return r.WithContext(ctx), nil 154 | } 155 | 156 | func setupRequestID(r *http.Request) *http.Request { 157 | id := uuid.New().String() 158 | caddyhttp.SetVar(r.Context(), core.VarReqID, id) 159 | return r 160 | } 161 | 162 | func renderTemplate(w http.ResponseWriter, r *http.Request, c *core.Config, baseURL string, header string, child templ.Component, opts ...func(*templ.ComponentHandler)) error { 163 | ctx := templ.WithChildren( 164 | context.WithValue( 165 | context.WithValue(r.Context(), web.BaseURLCtxKey, baseURL), 166 | web.VersionCtxKey, 167 | core.Version, 168 | ), 169 | child, 170 | ) 171 | templ.Handler( 172 | web.Base(c.Title, header), 173 | opts..., 174 | ).ServeHTTP(w, r.WithContext(ctx)) 175 | return nil 176 | } 177 | -------------------------------------------------------------------------------- /directives/endpoint.go: -------------------------------------------------------------------------------- 1 | package directives 2 | 3 | import ( 4 | "crypto/subtle" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/caddyserver/caddy/v2" 13 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 14 | "github.com/golang-jwt/jwt/v5" 15 | "github.com/sjtug/cerberus/core" 16 | "github.com/sjtug/cerberus/internal/ipblock" 17 | "github.com/sjtug/cerberus/web" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | // Endpoint is the handler that will be used to serve challenge endpoints and static files. 22 | type Endpoint struct { 23 | instance *core.Instance 24 | logger *zap.Logger 25 | } 26 | 27 | func checkAnswer(s string, difficulty int) bool { 28 | nibbles := difficulty / 2 29 | remaining := difficulty % 2 30 | 31 | if !strings.HasPrefix(s, strings.Repeat("0", nibbles)) { 32 | return false 33 | } 34 | 35 | if remaining == 0 { 36 | return true 37 | } 38 | 39 | return s[nibbles] < '8' 40 | } 41 | 42 | func (e *Endpoint) answerHandle(w http.ResponseWriter, r *http.Request) error { 43 | c := e.instance 44 | 45 | // Just to make sure the response is not cached, although this should be the default behavior for POST requests. 46 | w.Header().Set("Cache-Control", "no-cache") 47 | 48 | nonceStr := r.FormValue("nonce") 49 | if nonceStr == "" { 50 | e.logger.Info("nonce is empty") 51 | return respondFailure(w, r, &c.Config, "nonce is empty", false, http.StatusBadRequest, ".") 52 | } 53 | nonce64, err := strconv.ParseUint(nonceStr, 10, 32) 54 | if err != nil { 55 | e.logger.Debug("nonce is not an integer", zap.Error(err)) 56 | return respondFailure(w, r, &c.Config, "nonce is not an integer", false, http.StatusBadRequest, ".") 57 | } 58 | nonce := uint32(nonce64) 59 | if !c.InsertUsedNonce(nonce) { 60 | e.logger.Info("nonce already used") 61 | return respondFailure(w, r, &c.Config, "nonce already used", false, http.StatusBadRequest, ".") 62 | } 63 | 64 | tsStr := r.FormValue("ts") 65 | if tsStr == "" { 66 | e.logger.Info("ts is empty") 67 | return respondFailure(w, r, &c.Config, "ts is empty", false, http.StatusBadRequest, ".") 68 | } 69 | ts, err := strconv.ParseInt(tsStr, 10, 64) 70 | if err != nil { 71 | e.logger.Debug("ts is not a integer", zap.Error(err)) 72 | return respondFailure(w, r, &c.Config, "ts is not a integer", false, http.StatusBadRequest, ".") 73 | } 74 | now := time.Now().Unix() 75 | if ts < now-int64(core.NonceTTL) || ts > now { 76 | e.logger.Info("invalid ts", zap.Int64("ts", ts), zap.Int64("now", now)) 77 | return respondFailure(w, r, &c.Config, "invalid ts", false, http.StatusBadRequest, ".") 78 | } 79 | 80 | signature := r.FormValue("signature") 81 | if signature == "" { 82 | e.logger.Info("signature is empty") 83 | return respondFailure(w, r, &c.Config, "signature is empty", false, http.StatusBadRequest, ".") 84 | } 85 | 86 | solutionStr := r.FormValue("solution") 87 | if solutionStr == "" { 88 | e.logger.Info("solution is empty") 89 | return respondFailure(w, r, &c.Config, "solution is empty", false, http.StatusBadRequest, ".") 90 | } 91 | solution, err := strconv.Atoi(solutionStr) 92 | if err != nil { 93 | e.logger.Debug("solution is not a integer", zap.Error(err)) 94 | return respondFailure(w, r, &c.Config, "solution is not a integer", false, http.StatusBadRequest, ".") 95 | } 96 | 97 | response := r.FormValue("response") 98 | redir := r.FormValue("redir") 99 | 100 | challenge, err := challengeFor(r, c) 101 | if err != nil { 102 | e.logger.Error("failed to calculate challenge", zap.Error(err)) 103 | return err 104 | } 105 | 106 | expectedSignature := calcSignature(challenge, nonce, ts, c) 107 | if signature != expectedSignature { 108 | e.logger.Debug("signature mismatch", zap.String("expected", expectedSignature), zap.String("actual", signature)) 109 | return respondFailure(w, r, &c.Config, "signature mismatch", false, http.StatusForbidden, ".") 110 | } 111 | 112 | answer, err := blake3sum(fmt.Sprintf("%s|%d|%d|%s|%d", challenge, nonce, ts, signature, solution)) 113 | if err != nil { 114 | e.logger.Error("failed to calculate answer", zap.Error(err)) 115 | return err 116 | } 117 | 118 | if !checkAnswer(response, c.Difficulty) { 119 | clearCookie(w, c.CookieName) 120 | e.logger.Error("wrong response", zap.String("response", response), zap.Int("difficulty", c.Difficulty)) 121 | return respondFailure(w, r, &c.Config, "wrong response", false, http.StatusForbidden, ".") 122 | } 123 | 124 | if subtle.ConstantTimeCompare([]byte(answer), []byte(response)) != 1 { 125 | clearCookie(w, c.CookieName) 126 | e.logger.Error("response mismatch", zap.String("expected", answer), zap.String("actual", response)) 127 | return respondFailure(w, r, &c.Config, "response mismatch", false, http.StatusForbidden, ".") 128 | } 129 | 130 | // Now we know the user passed the challenge, we issue an approval and sign the result. 131 | approvalID := c.IssueApproval(c.AccessPerApproval) 132 | token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{ 133 | "challenge": challenge, 134 | "response": response, 135 | "approval_id": approvalID, 136 | "iat": time.Now().Unix(), 137 | "nbf": time.Now().Add(-time.Minute).Unix(), 138 | "exp": time.Now().Add(c.ApprovalTTL).Unix(), 139 | }) 140 | tokenStr, err := token.SignedString(c.GetPrivateKey()) 141 | if err != nil { 142 | e.logger.Error("failed to sign token", zap.Error(err)) 143 | return err 144 | } 145 | 146 | http.SetCookie(w, &http.Cookie{ 147 | Name: c.CookieName, 148 | Value: tokenStr, 149 | Expires: time.Now().Add(c.ApprovalTTL), 150 | SameSite: http.SameSiteLaxMode, 151 | Path: "/", 152 | }) 153 | 154 | e.logger.Debug("user passed the challenge") 155 | 156 | ipBlockRaw := caddyhttp.GetVar(r.Context(), core.VarIPBlock) 157 | if ipBlockRaw != nil { 158 | ipBlock := ipBlockRaw.(ipblock.IPBlock) 159 | c.DecPending(ipBlock) 160 | } 161 | 162 | w.Header().Set(c.HeaderName, "PASS") 163 | http.Redirect(w, r, redir, http.StatusSeeOther) 164 | return nil 165 | } 166 | 167 | // tryServeFile serves static files from the dist directory. 168 | func tryServeFile(w http.ResponseWriter, r *http.Request) bool { 169 | if !strings.HasPrefix(r.URL.Path, "/static/") { 170 | return false 171 | } 172 | 173 | // Remove the /static/ prefix to get the actual file path 174 | filePath := strings.TrimSuffix(caddyhttp.SanitizedPathJoin("/dist/", strings.TrimPrefix(r.URL.Path, "/static/")), "/") 175 | 176 | // Add cache control headers for static assets 177 | w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") // Cache for 1 year 178 | w.Header().Set("Vary", "Accept-Encoding") 179 | 180 | // Create a new request with the modified path 181 | req := *r 182 | req.URL.Path = filePath 183 | 184 | // Serve the file using http.FileServer 185 | http.FileServer(http.FS(web.Content)).ServeHTTP(w, &req) 186 | return true 187 | } 188 | 189 | func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error { 190 | r = setupRequestID(r) 191 | r, err := setupLocale(r) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | if tryServeFile(w, r) { 197 | return nil 198 | } 199 | 200 | c := e.instance 201 | 202 | path := strings.TrimSuffix(r.URL.Path, "/") 203 | if path == "/answer" && r.Method == http.MethodPost { 204 | return e.answerHandle(w, r) 205 | } 206 | 207 | return respondFailure(w, r, &c.Config, "Not found", false, http.StatusNotFound, ".") 208 | } 209 | 210 | func (e *Endpoint) Provision(ctx caddy.Context) error { 211 | e.logger = ctx.Logger() 212 | 213 | appRaw, err := ctx.App("cerberus") 214 | if err != nil { 215 | return err 216 | } 217 | app := appRaw.(*App) 218 | 219 | instance := app.GetInstance() 220 | if instance == nil { 221 | return errors.New("no global cerberus app found") 222 | } 223 | e.instance = instance 224 | 225 | return nil 226 | } 227 | 228 | func (Endpoint) CaddyModule() caddy.ModuleInfo { 229 | return caddy.ModuleInfo{ 230 | ID: "http.handlers.cerberus_endpoint", 231 | New: func() caddy.Module { return new(Endpoint) }, 232 | } 233 | } 234 | 235 | var ( 236 | _ caddy.Provisioner = (*Endpoint)(nil) 237 | _ caddyhttp.MiddlewareHandler = (*Endpoint)(nil) 238 | ) 239 | -------------------------------------------------------------------------------- /directives/i18n.go: -------------------------------------------------------------------------------- 1 | package directives 2 | 3 | import ( 4 | "io/fs" 5 | 6 | "github.com/invopop/ctxi18n" 7 | ) 8 | 9 | func LoadI18n(fs fs.FS) { 10 | if err := ctxi18n.LoadWithDefault(fs, "en"); err != nil { 11 | panic(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /directives/middleware.go: -------------------------------------------------------------------------------- 1 | package directives 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/caddyserver/caddy/v2" 11 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 12 | "github.com/golang-jwt/jwt/v5" 13 | "github.com/google/uuid" 14 | "github.com/invopop/ctxi18n/i18n" 15 | "github.com/sjtug/cerberus/core" 16 | "github.com/sjtug/cerberus/internal/ipblock" 17 | "github.com/sjtug/cerberus/internal/randpool" 18 | "github.com/sjtug/cerberus/web" 19 | "go.uber.org/zap" 20 | ) 21 | 22 | // Middleware is the actual middleware that will be used to challenge requests. 23 | type Middleware struct { 24 | // The base URL for the challenge. It must be the same as the deployed endpoint route. 25 | BaseURL string `json:"base_url,omitempty"` 26 | // If true, the middleware will not perform any challenge. It will only block known bad IPs. 27 | BlockOnly bool `json:"block_only,omitempty"` 28 | 29 | instance *core.Instance 30 | logger *zap.Logger 31 | } 32 | 33 | func getClientIP(r *http.Request) string { 34 | address := caddyhttp.GetVar(r.Context(), caddyhttp.ClientIPVarKey).(string) 35 | clientIP, _, err := net.SplitHostPort(address) 36 | if err != nil { 37 | clientIP = address // no port 38 | } 39 | 40 | return clientIP 41 | } 42 | 43 | func (m *Middleware) invokeAuth(w http.ResponseWriter, r *http.Request) error { 44 | c := m.instance 45 | 46 | // Make sure the response is not cached so that users always see the latest challenge. 47 | w.Header().Set("Cache-Control", "no-cache") 48 | 49 | ipBlockRaw := caddyhttp.GetVar(r.Context(), core.VarIPBlock) 50 | if ipBlockRaw != nil { 51 | ipBlock := ipBlockRaw.(ipblock.IPBlock) 52 | 53 | count := c.IncPending(ipBlock) 54 | if count > c.MaxPending { 55 | m.logger.Info( 56 | "Max failed/active challenges reached for IP block, rejecting", 57 | zap.String("ip", ipBlock.ToIPNet(c.PrefixCfg).String()), 58 | ) 59 | c.InsertBlocklist(ipBlock) 60 | c.RemovePending(ipBlock) 61 | 62 | return respondFailure(w, r, &c.Config, "IP blocked", true, http.StatusForbidden, m.BaseURL) 63 | } 64 | } 65 | 66 | clearCookie(w, c.CookieName) 67 | 68 | challenge, err := challengeFor(r, c) 69 | if err != nil { 70 | m.logger.Error("failed to calculate challenge", zap.Error(err)) 71 | return err 72 | } 73 | 74 | nonce := randpool.ReadUint32() 75 | ts := time.Now().Unix() 76 | signature := calcSignature(challenge, nonce, ts, c) 77 | 78 | w.Header().Set(c.HeaderName, "CHALLENGE") 79 | return renderTemplate(w, r, &c.Config, m.BaseURL, i18n.T(r.Context(), "challenge.title"), web.Challenge(challenge, c.Difficulty, nonce, ts, signature)) 80 | } 81 | 82 | func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { 83 | r = setupRequestID(r) 84 | r, err := setupLocale(r) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | c := m.instance 90 | 91 | if ipBlock, err := ipblock.NewIPBlock(net.ParseIP(getClientIP(r)), c.PrefixCfg); err == nil { 92 | caddyhttp.SetVar(r.Context(), core.VarIPBlock, ipBlock) 93 | if c.ContainsBlocklist(ipBlock) { 94 | m.logger.Debug("IP is blocked", zap.String("ip", ipBlock.ToIPNet(c.PrefixCfg).String())) 95 | return respondFailure(w, r, &c.Config, "", true, http.StatusForbidden, m.BaseURL) 96 | } 97 | } 98 | 99 | if m.BlockOnly { 100 | // If block only mode is enabled, we don't need to perform any challenge. 101 | // Continue to the next handler. 102 | w.Header().Set(c.HeaderName, "DISABLED") 103 | return next.ServeHTTP(w, r) 104 | } 105 | 106 | // Get the "cerberus-auth" cookie 107 | cookie, err := r.Cookie(c.CookieName) 108 | if err != nil { 109 | m.logger.Debug("cookie not found", zap.Error(err)) 110 | return m.invokeAuth(w, r) 111 | } 112 | 113 | if err := validateCookie(cookie); err != nil { 114 | m.logger.Debug("invalid cookie", zap.Error(err)) 115 | return m.invokeAuth(w, r) 116 | } 117 | 118 | token, err := jwt.ParseWithClaims(cookie.Value, jwt.MapClaims{}, func(_ *jwt.Token) (interface{}, error) { 119 | return c.GetPublicKey(), nil 120 | }, jwt.WithValidMethods([]string{jwt.SigningMethodEdDSA.Alg()})) 121 | if err != nil { 122 | m.logger.Debug("invalid token", zap.Error(err)) 123 | } 124 | 125 | if err := validateToken(token); err != nil { 126 | m.logger.Debug("invalid token", zap.Error(err)) 127 | return m.invokeAuth(w, r) 128 | } 129 | 130 | // Metadata structure correct. Now we need to check the approval. 131 | claims := token.Claims.(jwt.MapClaims) 132 | 133 | // First we check approval state. 134 | approvalIDRaw, ok := claims["approval_id"].(string) 135 | if !ok { 136 | m.logger.Debug("token does not contain valid approval_id claim") 137 | return m.invokeAuth(w, r) 138 | } 139 | 140 | approvalID, err := uuid.Parse(approvalIDRaw) 141 | if err != nil { 142 | m.logger.Debug("invalid approval_id", zap.String("approval_id", approvalIDRaw), zap.Error(err)) 143 | return m.invokeAuth(w, r) 144 | } 145 | 146 | approved := c.DecApproval(approvalID) 147 | if !approved { 148 | m.logger.Debug("approval not found", zap.String("approval_id", approvalIDRaw)) 149 | return m.invokeAuth(w, r) 150 | } 151 | 152 | // Then we check user fingerprint matches the challenge to prevent cookie reuse. 153 | challenge, ok := claims["challenge"].(string) 154 | if !ok { 155 | m.logger.Debug("token does not contain valid challenge claim") 156 | return m.invokeAuth(w, r) 157 | } 158 | 159 | expected, err := challengeFor(r, c) 160 | if err != nil { 161 | m.logger.Error("failed to calculate challenge", zap.Error(err)) 162 | return err 163 | } 164 | 165 | if challenge != expected { 166 | m.logger.Debug("challenge mismatch", zap.String("expected", expected), zap.String("actual", challenge)) 167 | return m.invokeAuth(w, r) 168 | } 169 | 170 | // OK: Continue to the next handler 171 | w.Header().Set(c.HeaderName, "PASS") 172 | return next.ServeHTTP(w, r) 173 | } 174 | 175 | func (m *Middleware) Provision(ctx caddy.Context) error { 176 | m.logger = ctx.Logger() 177 | 178 | appRaw, err := ctx.App("cerberus") 179 | if err != nil { 180 | return err 181 | } 182 | app := appRaw.(*App) 183 | 184 | instance := app.GetInstance() 185 | if instance == nil { 186 | return errors.New("no global cerberus app found") 187 | } 188 | m.instance = instance 189 | 190 | return nil 191 | } 192 | 193 | func (m *Middleware) Validate() error { 194 | if m.BaseURL == "" { 195 | return fmt.Errorf("base_url is required") 196 | } 197 | return nil 198 | } 199 | 200 | func (Middleware) CaddyModule() caddy.ModuleInfo { 201 | return caddy.ModuleInfo{ 202 | ID: "http.handlers.cerberus", 203 | New: func() caddy.Module { return new(Middleware) }, 204 | } 205 | } 206 | 207 | var ( 208 | _ caddy.Provisioner = (*Middleware)(nil) 209 | _ caddy.Validator = (*Middleware)(nil) 210 | _ caddyhttp.MiddlewareHandler = (*Middleware)(nil) 211 | ) 212 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sjtug/cerberus 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/a-h/templ v0.3.865 9 | github.com/caddyserver/caddy/v2 v2.10.0 10 | github.com/dustin/go-humanize v1.0.1 11 | github.com/golang-jwt/jwt/v5 v5.2.2 12 | github.com/invopop/ctxi18n v0.9.0 13 | github.com/zeebo/xxh3 v1.0.2 14 | go.uber.org/zap v1.27.0 15 | golang.org/x/crypto v0.38.0 16 | pgregory.net/rapid v1.2.0 17 | ) 18 | 19 | require ( 20 | cel.dev/expr v0.24.0 // indirect 21 | dario.cat/mergo v1.0.2 // indirect 22 | filippo.io/edwards25519 v1.1.0 // indirect 23 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 24 | github.com/KimMachineGun/automemlimit v0.7.2 // indirect 25 | github.com/Masterminds/goutils v1.1.1 // indirect 26 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 27 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 28 | github.com/Microsoft/go-winio v0.6.2 // indirect 29 | github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect 30 | github.com/andybalholm/brotli v1.1.0 // indirect 31 | github.com/antlr4-go/antlr/v4 v4.13.1 // indirect 32 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/caddyserver/certmagic v0.23.0 // indirect 35 | github.com/caddyserver/zerossl v0.1.3 // indirect 36 | github.com/ccoveille/go-safecast v1.6.1 // indirect 37 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 38 | github.com/cespare/xxhash v1.1.0 // indirect 39 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 40 | github.com/chzyer/readline v1.5.1 // indirect 41 | github.com/cli/browser v1.3.0 // indirect 42 | github.com/cloudflare/circl v1.6.1 // indirect 43 | github.com/coreos/go-oidc/v3 v3.14.1 // indirect 44 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 45 | github.com/dgraph-io/badger v1.6.2 // indirect 46 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect 47 | github.com/dgraph-io/ristretto v0.2.0 // indirect 48 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect 49 | github.com/elastic/go-freelru v0.16.0 50 | github.com/fatih/color v1.16.0 // indirect 51 | github.com/francoispqt/gojay v1.2.13 // indirect 52 | github.com/fsnotify/fsnotify v1.7.0 // indirect 53 | github.com/go-jose/go-jose/v3 v3.0.4 // indirect 54 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 55 | github.com/go-sql-driver/mysql v1.9.2 // indirect 56 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 57 | github.com/golang/protobuf v1.5.4 // indirect 58 | github.com/golang/snappy v1.0.0 // indirect 59 | github.com/google/cel-go v0.25.0 // indirect 60 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect 61 | github.com/google/uuid v1.6.0 62 | github.com/huandu/xstrings v1.5.0 // indirect 63 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 64 | github.com/invopop/yaml v0.3.1 // indirect 65 | github.com/jackc/pgpassfile v1.0.0 // indirect 66 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 67 | github.com/jackc/pgx/v5 v5.7.5 // indirect 68 | github.com/jackc/puddle/v2 v2.2.2 // indirect 69 | github.com/klauspost/compress v1.18.0 // indirect 70 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 71 | github.com/libdns/libdns v1.1.0 // indirect 72 | github.com/manifoldco/promptui v0.9.0 // indirect 73 | github.com/mattn/go-colorable v0.1.14 // indirect 74 | github.com/mattn/go-isatty v0.0.20 // indirect 75 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 76 | github.com/mholt/acmez/v3 v3.1.2 // indirect 77 | github.com/miekg/dns v1.1.66 // indirect 78 | github.com/mitchellh/copystructure v1.2.0 // indirect 79 | github.com/mitchellh/go-ps v1.0.0 // indirect 80 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 81 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 82 | github.com/natefinch/atomic v1.0.1 // indirect 83 | github.com/onsi/ginkgo/v2 v2.23.4 // indirect 84 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 85 | github.com/pkg/errors v0.9.1 // indirect 86 | github.com/prometheus/client_golang v1.22.0 // indirect 87 | github.com/prometheus/client_model v0.6.2 // indirect 88 | github.com/prometheus/common v0.64.0 // indirect 89 | github.com/prometheus/procfs v0.16.1 // indirect 90 | github.com/quic-go/qpack v0.5.1 // indirect 91 | github.com/quic-go/quic-go v0.52.0 // indirect 92 | github.com/rs/xid v1.6.0 // indirect 93 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 94 | github.com/shopspring/decimal v1.4.0 // indirect 95 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 96 | github.com/slackhq/nebula v1.9.5 // indirect 97 | github.com/smallstep/certificates v0.28.3 // indirect 98 | github.com/smallstep/cli-utils v0.12.1 // indirect 99 | github.com/smallstep/linkedca v0.23.0 // indirect 100 | github.com/smallstep/nosql v0.7.0 // indirect 101 | github.com/smallstep/pkcs7 v0.2.1 // indirect 102 | github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect 103 | github.com/smallstep/truststore v0.13.0 // indirect 104 | github.com/spf13/cast v1.8.0 // indirect 105 | github.com/spf13/cobra v1.9.1 // indirect 106 | github.com/spf13/pflag v1.0.6 // indirect 107 | github.com/stoewer/go-strcase v1.3.0 // indirect 108 | github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect 109 | github.com/urfave/cli v1.22.16 // indirect 110 | github.com/zeebo/blake3 v0.2.4 111 | go.etcd.io/bbolt v1.4.0 // indirect 112 | go.step.sm/crypto v0.64.0 // indirect 113 | go.uber.org/automaxprocs v1.6.0 // indirect 114 | go.uber.org/mock v0.5.2 // indirect 115 | go.uber.org/multierr v1.11.0 // indirect 116 | go.uber.org/zap/exp v0.3.0 // indirect 117 | golang.org/x/crypto/x509roots/fallback v0.0.0-20250515174705-ebc8e4631531 // indirect 118 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 119 | golang.org/x/mod v0.24.0 // indirect 120 | golang.org/x/net v0.40.0 // indirect 121 | golang.org/x/oauth2 v0.30.0 // indirect 122 | golang.org/x/sync v0.14.0 // indirect 123 | golang.org/x/sys v0.33.0 // indirect 124 | golang.org/x/term v0.32.0 // indirect 125 | golang.org/x/text v0.25.0 // indirect 126 | golang.org/x/time v0.11.0 // indirect 127 | golang.org/x/tools v0.33.0 // indirect 128 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect 129 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect 130 | google.golang.org/grpc v1.72.1 // indirect 131 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect 132 | google.golang.org/protobuf v1.36.6 // indirect 133 | gopkg.in/yaml.v3 v3.0.1 // indirect 134 | howett.net/plist v1.0.1 // indirect 135 | ) 136 | 137 | tool github.com/a-h/templ/cmd/templ 138 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= 2 | cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 3 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 5 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 6 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 7 | cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= 8 | cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= 9 | cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= 10 | cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= 11 | cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= 12 | cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 13 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 14 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 15 | cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs= 16 | cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= 17 | cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE= 18 | cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w= 19 | cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= 20 | cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= 21 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 22 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 23 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 24 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 25 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 26 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 27 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 28 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 29 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 30 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= 31 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 32 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 33 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 34 | github.com/KimMachineGun/automemlimit v0.7.2 h1:DyfHI7zLWmZPn2Wqdy2AgTiUvrGPmnYWgwhHXtAegX4= 35 | github.com/KimMachineGun/automemlimit v0.7.2/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 36 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 37 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 38 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 39 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 40 | github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= 41 | github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= 42 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= 43 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 44 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 45 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= 46 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 47 | github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= 48 | github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= 49 | github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A= 50 | github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= 51 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 52 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 53 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 54 | github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= 55 | github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= 56 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 57 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= 58 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 59 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 60 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 61 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 62 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 63 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 64 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 65 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 66 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 67 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 68 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 69 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 70 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 71 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 72 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 73 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 74 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 75 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 76 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 77 | github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg= 78 | github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= 79 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 80 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 81 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 82 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 83 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 84 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 85 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 86 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 87 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 88 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 89 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 90 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 91 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 92 | github.com/caddyserver/caddy/v2 v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U= 93 | github.com/caddyserver/caddy/v2 v2.10.0/go.mod h1:q+dgBS3xtIJJGYI2H5Nyh9+4BvhQQ9yCGmECv4Ubdjo= 94 | github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= 95 | github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= 96 | github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= 97 | github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 98 | github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= 99 | github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= 100 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 101 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 102 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 103 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 104 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 105 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 106 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 107 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 108 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 109 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 110 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 111 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 112 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 113 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 114 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 115 | github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= 116 | github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= 117 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 118 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 119 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 120 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 121 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 122 | github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= 123 | github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= 124 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 125 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 126 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 127 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 128 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 129 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 130 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 131 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 132 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 133 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 134 | github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= 135 | github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= 136 | github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= 137 | github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= 138 | github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 139 | github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 140 | github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= 141 | github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 142 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 143 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= 144 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 145 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 146 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 147 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 148 | github.com/elastic/go-freelru v0.16.0 h1:gG2HJ1WXN2tNl5/p40JS/l59HjvjRhjyAa+oFTRArYs= 149 | github.com/elastic/go-freelru v0.16.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I= 150 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 151 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 152 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 153 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 154 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 155 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 156 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 157 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 158 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 159 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 160 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 161 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 162 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 163 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 164 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 165 | github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 166 | github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 167 | github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= 168 | github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= 169 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 170 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 171 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 172 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 173 | github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= 174 | github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 175 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 176 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 177 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 178 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 179 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 180 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 181 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 182 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 183 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 184 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 185 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 186 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 187 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 188 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 189 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 190 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 191 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 192 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 193 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 194 | github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= 195 | github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 196 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= 197 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= 198 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 199 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 200 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 201 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 202 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 203 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 204 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 205 | github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= 206 | github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= 207 | github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= 208 | github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= 209 | github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= 210 | github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= 211 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 212 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 213 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4= 214 | github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 215 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 216 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 217 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 218 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 219 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= 220 | github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 221 | github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= 222 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 223 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 224 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 225 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 226 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 227 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 228 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 229 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 230 | github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= 231 | github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 232 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 233 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 234 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 235 | github.com/invopop/ctxi18n v0.9.0 h1:BIia4u4OngaHVn/7gvK0w6lccOXVtad8xU0KgJ+mnVA= 236 | github.com/invopop/ctxi18n v0.9.0/go.mod h1:1Osw+JGYA+anHt0Z4reF36r5FtGHYjGQ+m1X7keIhPc= 237 | github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= 238 | github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 239 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 240 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 241 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 242 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 243 | github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= 244 | github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 245 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 246 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 247 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 248 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 249 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 250 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 251 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 252 | github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 253 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 254 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 255 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 256 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 257 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 258 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 259 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 260 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 261 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 262 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 263 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 264 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 265 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 266 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 267 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 268 | github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= 269 | github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 270 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 271 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 272 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 273 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 274 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 275 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 276 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 277 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 278 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 279 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 280 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 281 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 282 | github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= 283 | github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= 284 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 285 | github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= 286 | github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= 287 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 288 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 289 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 290 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 291 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 292 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 293 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 294 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 295 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 296 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 297 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 298 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 299 | github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= 300 | github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= 301 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 302 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 303 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 304 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 305 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 306 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 307 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 308 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 309 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 310 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 311 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 312 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 313 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 314 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 315 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 316 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 317 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 318 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 319 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 320 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 321 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 322 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 323 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 324 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 325 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 326 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 327 | github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 328 | github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 329 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 330 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 331 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 332 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 333 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 334 | github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= 335 | github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= 336 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 337 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 338 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 339 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 340 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 341 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 342 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 343 | github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= 344 | github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= 345 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 346 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 347 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 348 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 349 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 350 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 351 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 352 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 353 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 354 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 355 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 356 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 357 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 358 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 359 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 360 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 361 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 362 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 363 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 364 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 365 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 366 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 367 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 368 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 369 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 370 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 371 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 372 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 373 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 374 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 375 | github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY= 376 | github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= 377 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= 378 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= 379 | github.com/smallstep/certificates v0.28.3 h1:rcMh1TAs8m2emP3aDJxKLkE9jriAtcFtCuj2gttnpmI= 380 | github.com/smallstep/certificates v0.28.3/go.mod h1:P/IjGTvRCem3YZ7d1XtUxpvK/8dfFsJn7gaVLpMXbJw= 381 | github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= 382 | github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= 383 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= 384 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= 385 | github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU= 386 | github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8= 387 | github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= 388 | github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= 389 | github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= 390 | github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= 391 | github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA= 392 | github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ= 393 | github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= 394 | github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= 395 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 396 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 397 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 398 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 399 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 400 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 401 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 402 | github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= 403 | github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 404 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 405 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 406 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 407 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 408 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 409 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 410 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 411 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 412 | github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= 413 | github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 414 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 415 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 416 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 417 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 418 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 419 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 420 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 421 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 422 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 423 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 424 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 425 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 426 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 427 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 428 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 429 | github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= 430 | github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= 431 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 432 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 433 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 434 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 435 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 436 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 437 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 438 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 439 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 440 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 441 | github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= 442 | github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= 443 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 444 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 445 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 446 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 447 | go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 448 | go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 449 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 450 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 451 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 452 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= 453 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= 454 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 455 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 456 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 457 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 458 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 459 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 460 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 461 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 462 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 463 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 464 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 465 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 466 | go.step.sm/crypto v0.64.0 h1:tZ2k9Am6v3Y7cZCn89uTt77BYYXqvw+5WekUX3WZiXQ= 467 | go.step.sm/crypto v0.64.0/go.mod h1:EEY+UgKKqsvydv4mvtSpW2fqu2ezvPcAzkC80DwxmrI= 468 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 469 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 470 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 471 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 472 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 473 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 474 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 475 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 476 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 477 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 478 | go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= 479 | go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= 480 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 481 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 482 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 483 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 484 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 485 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 486 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 487 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 488 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 489 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 490 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 491 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 492 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 493 | golang.org/x/crypto/x509roots/fallback v0.0.0-20250515174705-ebc8e4631531 h1:uEZjxClB4DwZIRL2pFsPPv0Y1rBtHvoqDVg96PJf30Y= 494 | golang.org/x/crypto/x509roots/fallback v0.0.0-20250515174705-ebc8e4631531/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= 495 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 496 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= 497 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= 498 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 499 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 500 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 501 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 502 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 503 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 504 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 505 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 506 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 507 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 508 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 509 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 510 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 511 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 512 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 513 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 514 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 515 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 516 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 517 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 518 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 519 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 520 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 521 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 522 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 523 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 524 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 525 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 526 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 527 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 528 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 529 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 530 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 531 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 532 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 533 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 534 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 535 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 536 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 537 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 538 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 539 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 540 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 541 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 542 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 543 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 544 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 545 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 546 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 547 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 548 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 549 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 550 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 551 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 552 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 553 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 554 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 555 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 556 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 557 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 558 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 559 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 560 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 561 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 562 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 563 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 564 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 565 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 566 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 567 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 568 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 569 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 570 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 571 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 572 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 573 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 574 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 575 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 576 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 577 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 578 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 579 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 580 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 581 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 582 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 583 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 584 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 585 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 586 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 587 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 588 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 589 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 590 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 591 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 592 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 593 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 594 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 595 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 596 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 597 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 598 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 599 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 600 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 601 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 602 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 603 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 604 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 605 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 606 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 607 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 608 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 609 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 610 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 611 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 612 | google.golang.org/api v0.232.0 h1:qGnmaIMf7KcuwHOlF3mERVzChloDYwRfOJOrHt8YC3I= 613 | google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= 614 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 615 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 616 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 617 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 618 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 619 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 620 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 621 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 622 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 623 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= 624 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= 625 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= 626 | google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= 627 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= 628 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 629 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 630 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 631 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 632 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 633 | google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 634 | google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 635 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= 636 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= 637 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 638 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 639 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 640 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 641 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 642 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 643 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 644 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 645 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 646 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 647 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 648 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 649 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 650 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 651 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 652 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 653 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 654 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 655 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 656 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 657 | howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= 658 | howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 659 | pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= 660 | pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= 661 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 662 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 663 | -------------------------------------------------------------------------------- /internal/expiremap/expiremap.go: -------------------------------------------------------------------------------- 1 | package expiremap 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var ( 10 | numShards = runtime.GOMAXPROCS(0) * 16 11 | ) 12 | 13 | type entry[V any] struct { 14 | value V 15 | expire time.Time 16 | } 17 | 18 | type shard[K comparable, V any] struct { 19 | mu sync.Mutex 20 | store map[K]entry[V] 21 | } 22 | 23 | type ExpireMap[K comparable, V any] struct { 24 | shards []*shard[K, V] 25 | hash func(K) uint32 26 | } 27 | 28 | // fastModulo calculates x % n without using the modulo operator (~4x faster). 29 | // Reference: https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ 30 | func fastModulo(x, n uint32) uint32 { 31 | return uint32((uint64(x) * uint64(n)) >> 32) //nolint:gosec 32 | } 33 | 34 | func NewExpireMap[K comparable, V any](hash func(K) uint32) *ExpireMap[K, V] { 35 | shards := make([]*shard[K, V], numShards) 36 | for i := range shards { 37 | shards[i] = &shard[K, V]{store: make(map[K]entry[V])} 38 | } 39 | return &ExpireMap[K, V]{shards: shards, hash: hash} 40 | } 41 | 42 | func (m *ExpireMap[K, V]) Get(key K) (*V, bool) { 43 | shard := m.shards[fastModulo(m.hash(key), uint32(len(m.shards)))] // #nosec G115 we don't have so many cores 44 | 45 | shard.mu.Lock() 46 | defer shard.mu.Unlock() 47 | 48 | value, ok := shard.store[key] 49 | if !ok { 50 | // Key not found 51 | return nil, false 52 | } 53 | 54 | if value.expire.Before(time.Now()) { 55 | // Key expired, remove it 56 | delete(shard.store, key) 57 | return nil, false 58 | } 59 | 60 | return &value.value, true 61 | } 62 | 63 | // SetIfAbsent sets the value for the key if it is not already present. 64 | // Returns true if the value was set, false if it was already present. 65 | func (m *ExpireMap[K, V]) SetIfAbsent(key K, value V, ttl time.Duration) bool { 66 | shard := m.shards[fastModulo(m.hash(key), uint32(len(m.shards)))] // #nosec G115 we don't have so many cores 67 | 68 | shard.mu.Lock() 69 | defer shard.mu.Unlock() 70 | 71 | if _, ok := shard.store[key]; ok { 72 | return false 73 | } 74 | 75 | shard.store[key] = entry[V]{value: value, expire: time.Now().Add(ttl)} 76 | return true 77 | } 78 | 79 | func (m *ExpireMap[K, V]) PurgeExpired() { 80 | now := time.Now() 81 | 82 | for _, shard := range m.shards { 83 | shard.mu.Lock() 84 | defer shard.mu.Unlock() 85 | 86 | for key, entry := range shard.store { 87 | if entry.expire.Before(now) { 88 | delete(shard.store, key) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/ipblock/ipblock.go: -------------------------------------------------------------------------------- 1 | package ipblock 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | // IPBlock represents either an IPv4 or IPv6 block 10 | // Data representation: 11 | // v6: Stored as first 8 bytes of the address 12 | // v4: Stored as 2001:db8: 13 | type IPBlock struct { 14 | data uint64 15 | } 16 | 17 | // IPBlockConfig represents the configuration for an IPBlock. 18 | // It's used to specify the prefix length for IPv4 and IPv6 blocks for IP blocking. 19 | type Config struct { 20 | // V4Prefix is the prefix length for IPv4 blocks 21 | V4Prefix int `json:"v4_prefix"` 22 | // V6Prefix is the prefix length for IPv6 blocks 23 | V6Prefix int `json:"v6_prefix"` 24 | } 25 | 26 | func (c Config) IsEmpty() bool { 27 | return c.V4Prefix == 0 && c.V6Prefix == 0 28 | } 29 | 30 | func ValidateConfig(cfg Config) error { 31 | if cfg.V4Prefix > 32 || cfg.V4Prefix < 1 { 32 | return fmt.Errorf("v4_prefix must be between 1 and 32, got %d", cfg.V4Prefix) 33 | } else if cfg.V6Prefix > 64 || cfg.V6Prefix < 1 { 34 | // Due to uint64 size limitation, we only allow at most /64 for IPv6 35 | return fmt.Errorf("v6_prefix must be between 1 and 64, got %d", cfg.V6Prefix) 36 | } 37 | return nil 38 | } 39 | 40 | // NewIPBlock creates a new IPBlock from an IP address 41 | func NewIPBlock(ip net.IP, cfg Config) (IPBlock, error) { 42 | if ip == nil { 43 | return IPBlock{}, errors.New("invalid IP: nil") 44 | } 45 | 46 | ip4 := ip.To4() 47 | if ip4 != nil { 48 | ip4 = ip4.Mask(net.CIDRMask(cfg.V4Prefix, 32)) 49 | return IPBlock{ 50 | data: 0x20010db800000000 | uint64(ip4[0])<<24 | uint64(ip4[1])<<16 | uint64(ip4[2])<<8 | uint64(ip4[3]), 51 | }, nil 52 | } 53 | 54 | ip6 := ip.To16() 55 | if ip6 == nil { 56 | return IPBlock{}, fmt.Errorf("invalid IP: %v", ip) 57 | } 58 | ip6 = ip6.Mask(net.CIDRMask(cfg.V6Prefix, 128)) 59 | data := uint64(0) 60 | for i := range 8 { 61 | data = data<<8 | uint64(ip6[i]) 62 | } 63 | return IPBlock{data: data}, nil 64 | } 65 | 66 | func (b IPBlock) ToUint64() uint64 { 67 | return b.data 68 | } 69 | 70 | func (b IPBlock) ToIPNet(cfg Config) *net.IPNet { 71 | if b.data&0xffffffff00000000 == 0x20010db800000000 { 72 | return &net.IPNet{ 73 | IP: net.IPv4(byte(b.data>>24&0xff), byte(b.data>>16&0xff), byte(b.data>>8&0xff), byte(b.data&0xff)), 74 | Mask: net.CIDRMask(cfg.V4Prefix, 32), 75 | } 76 | } 77 | 78 | ip := make(net.IP, 16) 79 | for i := range 8 { 80 | ip[7-i] = byte(b.data >> (8 * i)) 81 | } 82 | return &net.IPNet{ 83 | IP: ip, 84 | Mask: net.CIDRMask(cfg.V6Prefix, 128), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/ipblock/ipblock_test.go: -------------------------------------------------------------------------------- 1 | package ipblock 2 | 3 | import ( 4 | "net" 5 | "slices" 6 | "testing" 7 | 8 | "pgregory.net/rapid" 9 | ) 10 | 11 | func TestIpBlock_spec(t *testing.T) { 12 | v4Gen := rapid.Custom(func(t *rapid.T) net.IP { 13 | return net.IPv4( 14 | rapid.Uint8().Draw(t, "v4_a"), 15 | rapid.Uint8().Draw(t, "v4_b"), 16 | rapid.Uint8().Draw(t, "v4_c"), 17 | rapid.Uint8().Draw(t, "v4_d"), 18 | ) 19 | }) 20 | v6Gen := rapid.Custom(func(t *rapid.T) net.IP { 21 | return net.IP(rapid.SliceOfN(rapid.Byte(), 16, 16).Draw(t, "v6")) 22 | }).Filter(func(ip net.IP) bool { 23 | // Make sure it's not of 2001:db8::/32 24 | return ip[0] != 0x20 || ip[1] != 0x01 || ip[2] != 0x0d || ip[3] != 0xb8 25 | }) 26 | IPGen := rapid.Custom(func(t *rapid.T) net.IP { 27 | selV4 := rapid.Bool().Draw(t, "selV4") 28 | if selV4 { 29 | return v4Gen.Draw(t, "v4") 30 | } 31 | return v6Gen.Draw(t, "v6") 32 | }).Filter(func(ip net.IP) bool { 33 | return !ip.IsUnspecified() 34 | }) 35 | cfgGen := rapid.Custom(func(t *rapid.T) Config { 36 | return Config{ 37 | V4Prefix: rapid.IntRange(1, 32).Draw(t, "v4_prefix"), 38 | V6Prefix: rapid.IntRange(1, 64).Draw(t, "v6_prefix"), 39 | } 40 | }) 41 | rapid.Check(t, func(t *rapid.T) { 42 | ip := IPGen.Draw(t, "ip") 43 | cfg := cfgGen.Draw(t, "cfg") 44 | var expected net.IPNet 45 | if ip.To4() != nil { 46 | expected = net.IPNet{ 47 | IP: ip.Mask(net.CIDRMask(cfg.V4Prefix, 32)), 48 | Mask: net.CIDRMask(cfg.V4Prefix, 32), 49 | } 50 | } else { 51 | expected = net.IPNet{ 52 | IP: ip.Mask(net.CIDRMask(cfg.V6Prefix, 128)), 53 | Mask: net.CIDRMask(cfg.V6Prefix, 128), 54 | } 55 | } 56 | 57 | block, err := NewIPBlock(ip, cfg) 58 | if err != nil { 59 | t.Fatalf("failed to create IPBlock: %v", err) 60 | } 61 | 62 | actual := block.ToIPNet(cfg) 63 | if !actual.IP.Equal(expected.IP) || !slices.Equal(actual.Mask, expected.Mask) { 64 | t.Fatalf("expected %s, got %s", expected.String(), actual.String()) 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/randpool/randpool.go: -------------------------------------------------------------------------------- 1 | package randpool 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "io" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | poolSize = 16 * 16 12 | ) 13 | 14 | var ( 15 | poolMu sync.Mutex 16 | pool [poolSize]byte 17 | poolPos = poolSize 18 | ) 19 | 20 | func ReadUint32() uint32 { 21 | poolMu.Lock() 22 | defer poolMu.Unlock() 23 | 24 | if poolPos == poolSize { 25 | _, err := io.ReadFull(rand.Reader, pool[:]) 26 | if err != nil { 27 | panic(err) 28 | } 29 | poolPos = 0 30 | } 31 | 32 | poolPos += 4 33 | 34 | return binary.BigEndian.Uint32(pool[poolPos-4 : poolPos]) 35 | } 36 | -------------------------------------------------------------------------------- /pow/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /pow/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pow" 3 | version = "0.1.0" 4 | authors = ["Yanning Chen "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | wasm-bindgen = "0.2" 15 | js-sys = "0.3" 16 | web-sys = { version = "0.3", features = [ 17 | "Worker", 18 | "DedicatedWorkerGlobalScope", 19 | ] } 20 | blake3 = { version = "1.8", default-features = false } 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde-wasm-bindgen = "0.6" 23 | # The `console_error_panic_hook` crate provides better debugging of panics by 24 | # logging them with `console.error`. This is great for development, but requires 25 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 26 | # code size when deploying. 27 | console_error_panic_hook = { version = "0.1.7", optional = true } 28 | hex = "0.4.3" 29 | 30 | [dev-dependencies] 31 | wasm-bindgen-test = "0.3.34" 32 | 33 | [profile.release] 34 | # Tell `rustc` to optimize for small code size. 35 | opt-level = "s" 36 | lto = true 37 | -------------------------------------------------------------------------------- /pow/src/check_dubit.rs: -------------------------------------------------------------------------------- 1 | pub fn check_leading_zero_dubits(n: usize) -> fn(&[u8; 32], usize) -> bool { 2 | match n { 3 | 0..=16 => check_small, 4 | _ => check_general, 5 | } 6 | } 7 | 8 | fn check_small(hash: &[u8; 32], n: usize) -> bool { 9 | let first_word: u32 = (hash[0] as u32) << 24 | (hash[1] as u32) << 16 | (hash[2] as u32) << 8 | (hash[3] as u32); 10 | first_word.leading_zeros() >= (n as u32 * 2) 11 | } 12 | 13 | fn check_general(hash: &[u8; 32], n: usize) -> bool { 14 | panic!("I'm lazy") 15 | } -------------------------------------------------------------------------------- /pow/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod check_dubit; 2 | mod utils; 3 | 4 | use blake3::Hasher; 5 | use check_dubit::check_leading_zero_dubits; 6 | use serde::Serialize; 7 | use utils::set_panic_hook; 8 | use wasm_bindgen::prelude::*; 9 | use web_sys::DedicatedWorkerGlobalScope; 10 | 11 | fn worker_global_scope() -> DedicatedWorkerGlobalScope { 12 | let global = js_sys::global(); 13 | global.dyn_into().expect("not running in a web worker") 14 | } 15 | 16 | #[derive(Debug, Serialize)] 17 | struct Resp { 18 | hash: String, 19 | data: String, 20 | difficulty: u32, 21 | nonce: u32, 22 | } 23 | 24 | #[wasm_bindgen(start)] 25 | fn start() { 26 | set_panic_hook(); 27 | } 28 | 29 | #[wasm_bindgen] 30 | pub fn process_task(data: &str, difficulty: u32, thread_id: u32, threads: u32) { 31 | let worker = worker_global_scope(); 32 | 33 | let dubit_checker = check_leading_zero_dubits(difficulty as usize); 34 | 35 | let mut hasher = Hasher::new(); 36 | 37 | for i in (thread_id..).step_by(threads as usize) { 38 | hasher.reset(); 39 | 40 | let attempt = format!("{}{}", data, i); 41 | hasher.update(attempt.as_bytes()); 42 | let hash = hasher.finalize(); 43 | 44 | if dubit_checker(hash.as_bytes(), difficulty as usize) { 45 | let resp = Resp { 46 | hash: hex::encode(hash.as_bytes()), 47 | data: attempt, 48 | difficulty, 49 | nonce: i, 50 | }; 51 | worker 52 | .post_message( 53 | &serde_wasm_bindgen::to_value(&resp).expect("Failed to serialize response"), 54 | ) 55 | .expect("Failed to send message"); 56 | return; 57 | } 58 | 59 | // send a progress update every 1023 iterations. since each thread checks 60 | // separate values, one simple way to do this is by bit masking the 61 | // nonce for multiples of 1023. unfortunately, if the number of threads 62 | // is not prime, only some of the threads will be sending the status 63 | // update and they will get behind the others. this is slightly more 64 | // complicated but ensures an even distribution between threads. 65 | if i + threads > i | 8191 && (i >> 13) % threads == thread_id { 66 | worker 67 | .post_message(&JsValue::from_f64(f64::from(i))) 68 | .expect("Failed to send message"); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pow/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /translations/en.yaml: -------------------------------------------------------------------------------- 1 | en: 2 | challenge: 3 | title: "Making sure you're not a bot!" 4 | calculating: "Performing browser checks..." 5 | difficulty_speed: "Difficulty: %{difficulty}, Speed: %{speed}kH/s" 6 | taking_longer: "This is taking longer than expected. Please do not refresh the page." 7 | why_seeing: "Why am I seeing this?" 8 | why_seeing_body: 9 | part_1: >- 10 | You are seeing this because the administrator of this website has set up %{cerberus} to protect the server against abusive traffic. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone. 11 | part_2: >- 12 | If you're familiar with %{anubis} by %{techaro}, Cerberus is similar - it performs a PoW challenge to verify the request. While Anubis focuses on protecting websites from AI scrapers, Cerberus takes a much more aggressive approach to protect our open-source infrastructure. 13 | part_3: >- 14 | Please note that Cerberus requires the use of modern JavaScript features that plugins like %{jshelter} will disable. Please disable %{jshelter} or other such plugins for this domain. 15 | must_enable_js: "You must enable JavaScript to proceed." 16 | success: 17 | title: "Success!" 18 | verification_complete: "Verification Complete!" 19 | took_time_iterations: "Took %{time}ms, %{iterations} iterations" 20 | error: 21 | error_occurred: "Error occurred while processing your request" 22 | access_restricted: "Access has been restricted" 23 | browser_config_or_bug: "There might be an issue with your browser configuration, or something is wrong on our side." 24 | ip_blocked: "You (or your local network) have been blocked due to suspicious activity." 25 | wait_before_retry: "Please wait a while before you try again; in some cases this may take a few hours." 26 | contact_us: "If you believe this is an error, please contact us at %{mail}. Attach the request ID shown below to help us investigate." 27 | footer: 28 | author: Protected by %{cerberus} from %{sjtug}. 29 | upstream: Heavily inspired by %{anubis} from %{techaro} in 🇨🇦. 30 | -------------------------------------------------------------------------------- /translations/zh.yaml: -------------------------------------------------------------------------------- 1 | # I know I should split this into zh-Hans and zh-Hant, but @messageformat/convert only supports CLDR plural categories 2 | zh: 3 | challenge: 4 | title: "验证您不是机器人" 5 | calculating: "正在进行浏览器检查..." 6 | difficulty_speed: "难度:%{difficulty},速度:%{speed}kH/s" 7 | taking_longer: "验证时间超出预期,请勿刷新页面" 8 | why_seeing: "为什么我会看到这个页面?" 9 | why_seeing_body: 10 | part_1: >- 11 | 您看到这个页面是因为网站管理员启用了 %{cerberus} 来防御异常流量攻击。这类攻击可能导致网站服务中断,影响所有用户的正常访问。 12 | part_2: >- 13 | 如果您了解 %{techaro} 开发的 %{anubis},那么 Cerberus 采用了类似的 PoW 验证技术。不同的是,Anubis 主要针对 AI 爬虫,而 Cerberus 则采用了更激进的策略来保护我们的开源基础设施。 14 | part_3: >- 15 | 请注意,Cerberus 需要启用现代 JavaScript 功能,而 %{jshelter} 等插件会禁用这些功能。请为本域名禁用 %{jshelter} 或类似的插件。 16 | must_enable_js: "请启用 JavaScript 以继续访问" 17 | success: 18 | title: "验证成功" 19 | verification_complete: "验证已完成" 20 | took_time_iterations: "用时 %{time}ms,完成 %{iterations} 次迭代" 21 | error: 22 | error_occurred: "发生错误" 23 | access_restricted: "访问受限" 24 | browser_config_or_bug: "可能是浏览器配置问题,也可能是我们的系统出现了异常" 25 | ip_blocked: "由于检测到可疑活动,您的 IP 地址或本地网络已被封禁" 26 | wait_before_retry: "请稍后再试,某些情况下可能需要等待数小时" 27 | contact_us: "如有问题,请通过 %{mail} 联系我们。请附上下方显示的 request ID,以便我们进行排查。" 28 | footer: 29 | author: "由 %{sjtug} 开发的 %{cerberus} 提供保护" 30 | upstream: "灵感来源于 🇨🇦 %{techaro} 开发的 %{anubis}" 31 | -------------------------------------------------------------------------------- /web/embed.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed dist 8 | var Content embed.FS 9 | -------------------------------------------------------------------------------- /web/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss" source("."); 2 | @source not "dist"; 3 | 4 | body { 5 | background-color: #f4e9d5; 6 | } -------------------------------------------------------------------------------- /web/img/mascot-fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjtug/cerberus/b36f0606ea418188a68c1128d094513859515c76/web/img/mascot-fail.png -------------------------------------------------------------------------------- /web/img/mascot-pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjtug/cerberus/b36f0606ea418188a68c1128d094513859515c76/web/img/mascot-pass.png -------------------------------------------------------------------------------- /web/img/mascot-puzzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjtug/cerberus/b36f0606ea418188a68c1128d094513859515c76/web/img/mascot-puzzle.png -------------------------------------------------------------------------------- /web/index.templ: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 7 | "github.com/invopop/ctxi18n/i18n" 8 | "github.com/sjtug/cerberus/core" 9 | ) 10 | 11 | type contextKeyType int 12 | 13 | const ( 14 | BaseURLCtxKey contextKeyType = iota 15 | VersionCtxKey 16 | LocaleCtxKey 17 | ) 18 | 19 | templ T(key string, args ...any) { 20 | { i18n.T(ctx, key, args...) } 21 | } 22 | 23 | templ UnsafeT(key string, args ...any) { 24 | @templ.Raw(i18n.T(ctx, key, args...)) 25 | } 26 | 27 | func GetBaseURL(ctx context.Context) string { 28 | return ctx.Value(BaseURLCtxKey).(string) 29 | } 30 | 31 | func GetVersion(ctx context.Context) string { 32 | return ctx.Value(VersionCtxKey).(string) 33 | } 34 | 35 | func GetLocale(ctx context.Context) string { 36 | return ctx.Value(LocaleCtxKey).(string) 37 | } 38 | 39 | func GetRequestID(ctx context.Context) string { 40 | return caddyhttp.GetVar(ctx, core.VarReqID).(string) 41 | } 42 | 43 | templ Base(title string, header string) { 44 | 45 | 46 | 47 | 48 | 49 | { title } 50 | 53 |
54 |

{ header }

55 | { children... } 56 |

57 | @UnsafeT("footer.author", i18n.M{ 58 | "cerberus": `Cerberus`, 59 | "sjtug": `SJTUG`}) 60 |

61 |

62 | @UnsafeT("footer.upstream", i18n.M{ 63 | "anubis": `Anubis`, 64 | "techaro": `Techaro`}) 65 |

66 |
67 |

68 | request_id: { GetRequestID(ctx) } 69 |

70 | 71 | 72 | } 73 | 74 | templ Challenge(challenge string, difficulty int, nonce uint32, ts int64, signature string) { 75 | {{ 76 | challengeInput := struct { 77 | Challenge string `json:"challenge"` 78 | Difficulty int `json:"difficulty"` 79 | Nonce uint32 `json:"nonce"` 80 | TS int64 `json:"ts"` 81 | Signature string `json:"signature"` 82 | }{challenge, difficulty, nonce, ts, signature} 83 | 84 | baseURL := GetBaseURL(ctx) 85 | version := GetVersion(ctx) 86 | locale := GetLocale(ctx) 87 | metaInput := struct { 88 | BaseURL string `json:"baseURL"` 89 | Version string `json:"version"` 90 | Locale string `json:"locale"` 91 | }{baseURL, version, locale} 92 | 93 | relPath := func(path string) string { 94 | return baseURL + path + "?v=" + version 95 | } 96 | }} 97 | Cute anime mascot character 98 |
99 |

status

100 |

metrics

101 |

message

102 | 103 |
104 |
105 |
106 |
107 |
108 | 109 | @T("challenge.why_seeing") 110 | 111 |
112 |

113 | @UnsafeT("challenge.why_seeing_body.part_1", i18n.M{ 114 | "cerberus": `Cerberus`, 115 | }) 116 |

117 |

118 | @UnsafeT("challenge.why_seeing_body.part_2", i18n.M{ 119 | "anubis": `Anubis`, 120 | "techaro": `Techaro`, 121 | }) 122 |

123 |

124 | @UnsafeT("challenge.why_seeing_body.part_3", i18n.M{ 125 | "jshelter": `JShelter`, 126 | }) 127 |

128 |
129 |
130 | 135 | 136 | } 137 | 138 | templ Error(message string, description string, mail string) { 139 | 140 |

{ message }

141 |

{ description }

142 | if mail != "" { 143 |

144 | @UnsafeT("error.contact_us", i18n.M{"mail": `` + mail + ``}) 145 |

146 | } 147 | } 148 | -------------------------------------------------------------------------------- /web/js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /web/js/convert.js: -------------------------------------------------------------------------------- 1 | import { parse } from "yaml"; 2 | import fs from "fs"; 3 | import { globSync } from "glob"; 4 | 5 | import convert from "@messageformat/convert"; 6 | import MessageFormat from "@messageformat/core"; 7 | import compileModule from "@messageformat/core/compile-module.js"; 8 | 9 | const files = globSync(process.argv[2] + "/*.yaml"); 10 | const yamlData = Object.assign( 11 | {}, 12 | ...files.map((file) => parse(fs.readFileSync(file, "utf8"))) 13 | ); 14 | 15 | const { locales, translations } = convert(yamlData); 16 | 17 | const compiled = compileModule(new MessageFormat(locales), translations); 18 | fs.writeFileSync(process.argv[3], compiled); 19 | -------------------------------------------------------------------------------- /web/js/main.mjs: -------------------------------------------------------------------------------- 1 | // This file contains code adapted from https://github.com/TecharoHQ/anubis under the MIT License. 2 | 3 | import pow from "./pow.mjs"; 4 | import Messages from "@messageformat/runtime/messages" 5 | import msgData from "./icu/compiled.mjs" 6 | 7 | const messages = new Messages(msgData) 8 | console.log(messages.locale, messages.availableLocales); 9 | 10 | function t(key, props) { 11 | return messages.get(key.split('.'), props) 12 | } 13 | 14 | const meta = { 15 | baseURL: "", 16 | version: "", 17 | locale: "" 18 | } 19 | 20 | const dom = { 21 | title: document.getElementById('title'), 22 | mascot: document.getElementById('mascot'), 23 | status: document.getElementById('status'), 24 | metrics: document.getElementById('metrics'), 25 | message: document.getElementById('message'), 26 | progressContainer: document.getElementById('progress-container'), 27 | progressBar: document.getElementById('progress-bar') 28 | } 29 | 30 | const ui = { 31 | title: (title) => dom.title.textContent = title, 32 | mascotState: (state) => dom.mascot.src = `${meta.baseURL}/static/img/mascot-${state}.png?v=${meta.version}`, 33 | status: (status) => dom.status.textContent = status, 34 | metrics: (metrics) => dom.metrics.textContent = metrics, 35 | message: (message) => dom.message.textContent = message, 36 | progress: (progress) => { 37 | dom.progressContainer.classList.toggle('hidden', !progress); 38 | dom.progressBar.style.width = `${progress}%`; 39 | } 40 | } 41 | 42 | function createAnswerForm(hash, solution, baseURL, nonce, ts, signature) { 43 | function addHiddenInput(form, name, value) { 44 | const input = document.createElement('input'); 45 | input.type = 'hidden'; 46 | input.name = name; 47 | input.value = value; 48 | form.appendChild(input); 49 | } 50 | 51 | const form = document.createElement('form'); 52 | form.method = 'POST'; 53 | form.action = `${baseURL}/answer`; 54 | 55 | addHiddenInput(form, 'response', hash); 56 | addHiddenInput(form, 'solution', solution); 57 | addHiddenInput(form, 'nonce', nonce); 58 | addHiddenInput(form, 'ts', ts); 59 | addHiddenInput(form, 'signature', signature); 60 | addHiddenInput(form, 'redir', window.location.href); 61 | 62 | document.body.appendChild(form); 63 | return form; 64 | } 65 | 66 | (async () => { 67 | // const image = document.getElementById('image'); 68 | // const spinner = document.getElementById('spinner'); 69 | // const anubisVersion = JSON.parse(document.getElementById('anubis_version').textContent); 70 | 71 | const thisScript = document.getElementById('challenge-script'); 72 | const { challenge, difficulty, nonce: inputNonce, ts, signature } = JSON.parse(thisScript.getAttribute('x-challenge')); 73 | const { baseURL, version, locale } = JSON.parse(thisScript.getAttribute('x-meta')); 74 | 75 | // Initialize UI 76 | meta.baseURL = baseURL; 77 | meta.version = version; 78 | meta.locale = locale; 79 | 80 | // Set locale 81 | messages.locale = locale; 82 | 83 | // Set initial checking state 84 | ui.title(t('challenge.title')); 85 | ui.mascotState('puzzle'); 86 | ui.status(t('challenge.calculating')); 87 | ui.metrics(t('challenge.difficulty_speed', { difficulty, speed: 0 })); 88 | ui.message(''); 89 | ui.progress(0); 90 | 91 | const t0 = Date.now(); 92 | let lastUpdate = 0; 93 | 94 | const likelihood = Math.pow(16, -difficulty/2); 95 | 96 | const mergedChallenge = `${challenge}|${inputNonce}|${ts}|${signature}|`; 97 | const { hash, nonce: solution } = await pow(mergedChallenge, difficulty, null, (iters) => { 98 | // the probability of still being on the page is (1 - likelihood) ^ iters. 99 | // by definition, half of the time the progress bar only gets to half, so 100 | // apply a polynomial ease-out function to move faster in the beginning 101 | // and then slow down as things get increasingly unlikely. quadratic felt 102 | // the best in testing, but this may need adjustment in the future. 103 | const probability = Math.pow(1 - likelihood, iters); 104 | const distance = (1 - Math.pow(probability, 2)) * 100; 105 | 106 | // Update progress every 200ms 107 | const now = Date.now(); 108 | const delta = now - t0; 109 | 110 | if (delta - lastUpdate > 200) { 111 | const speed = iters / delta; 112 | ui.progress(distance); 113 | ui.metrics(t('challenge.difficulty_speed', { difficulty, speed: speed.toFixed(3) })); 114 | ui.message(probability < 0.01 ? t('challenge.taking_longer') : undefined); 115 | lastUpdate = delta; 116 | }; 117 | }); 118 | const t1 = Date.now(); 119 | console.log({ hash, solution }); 120 | 121 | // Show success state 122 | ui.title(t('success.title')); 123 | ui.mascotState('pass'); 124 | ui.status(t('success.verification_complete')); 125 | ui.metrics(t('success.took_time_iterations', { time: t1 - t0, iterations: solution })); 126 | ui.message(''); 127 | ui.progress(0); 128 | 129 | const form = createAnswerForm(hash, solution, baseURL, inputNonce, ts, signature); 130 | setTimeout(() => { 131 | form.submit(); 132 | }, 250); 133 | 134 | })(); -------------------------------------------------------------------------------- /web/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerberus-js", 3 | "version": "0.0.1", 4 | "private": true, 5 | "source": "main.mjs", 6 | "browserslist": "last 2 versions, not dead, > 0.2%", 7 | "type": "module", 8 | "dependencies": { 9 | "@messageformat/convert": "^1.0.0", 10 | "@messageformat/core": "^3.4.0", 11 | "@messageformat/runtime": "^3.0.1", 12 | "glob": "^11.0.2", 13 | "pow-wasm": "file:../../pow/pkg", 14 | "yaml": "^2.7.1" 15 | } 16 | } -------------------------------------------------------------------------------- /web/js/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@messageformat/convert': 12 | specifier: ^1.0.0 13 | version: 1.0.0 14 | '@messageformat/core': 15 | specifier: ^3.4.0 16 | version: 3.4.0 17 | '@messageformat/runtime': 18 | specifier: ^3.0.1 19 | version: 3.0.1 20 | glob: 21 | specifier: ^11.0.2 22 | version: 11.0.2 23 | pow-wasm: 24 | specifier: file:../../pow/pkg 25 | version: pow@file:../../pow/pkg 26 | yaml: 27 | specifier: ^2.7.1 28 | version: 2.7.1 29 | 30 | packages: 31 | 32 | '@isaacs/cliui@8.0.2': 33 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 34 | engines: {node: '>=12'} 35 | 36 | '@messageformat/convert@1.0.0': 37 | resolution: {integrity: sha512-IwYw/by/n8VOE+sZin2ysWW9ZFFv8I9p7fijN1H50wLbVv8rgBYTHIw1dIHBaTAQpKFa037qWWghRI4Vyn1+Ng==} 38 | engines: {node: '>=6.0'} 39 | 40 | '@messageformat/core@3.4.0': 41 | resolution: {integrity: sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==} 42 | 43 | '@messageformat/date-skeleton@1.1.0': 44 | resolution: {integrity: sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==} 45 | 46 | '@messageformat/number-skeleton@1.2.0': 47 | resolution: {integrity: sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==} 48 | 49 | '@messageformat/parser@5.1.1': 50 | resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} 51 | 52 | '@messageformat/runtime@3.0.1': 53 | resolution: {integrity: sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==} 54 | 55 | ansi-regex@5.0.1: 56 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 57 | engines: {node: '>=8'} 58 | 59 | ansi-regex@6.1.0: 60 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 61 | engines: {node: '>=12'} 62 | 63 | ansi-styles@4.3.0: 64 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 65 | engines: {node: '>=8'} 66 | 67 | ansi-styles@6.2.1: 68 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 69 | engines: {node: '>=12'} 70 | 71 | balanced-match@1.0.2: 72 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 73 | 74 | brace-expansion@2.0.1: 75 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 76 | 77 | color-convert@2.0.1: 78 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 79 | engines: {node: '>=7.0.0'} 80 | 81 | color-name@1.1.4: 82 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 83 | 84 | cross-spawn@7.0.6: 85 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 86 | engines: {node: '>= 8'} 87 | 88 | eastasianwidth@0.2.0: 89 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 90 | 91 | emoji-regex@8.0.0: 92 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 93 | 94 | emoji-regex@9.2.2: 95 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 96 | 97 | foreground-child@3.3.1: 98 | resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} 99 | engines: {node: '>=14'} 100 | 101 | glob@11.0.2: 102 | resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==} 103 | engines: {node: 20 || >=22} 104 | hasBin: true 105 | 106 | is-fullwidth-code-point@3.0.0: 107 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 108 | engines: {node: '>=8'} 109 | 110 | isexe@2.0.0: 111 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 112 | 113 | jackspeak@4.1.0: 114 | resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} 115 | engines: {node: 20 || >=22} 116 | 117 | lru-cache@11.1.0: 118 | resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} 119 | engines: {node: 20 || >=22} 120 | 121 | make-plural@6.2.2: 122 | resolution: {integrity: sha512-8iTuFioatnTTmb/YJjywkVIHLjcwkFD9Ms0JpxjEm9Mo8eQYkh1z+55dwv4yc1jQ8ftVBxWQbihvZL1DfzGGWA==} 123 | 124 | make-plural@7.4.0: 125 | resolution: {integrity: sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==} 126 | 127 | minimatch@10.0.1: 128 | resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} 129 | engines: {node: 20 || >=22} 130 | 131 | minipass@7.1.2: 132 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 133 | engines: {node: '>=16 || 14 >=14.17'} 134 | 135 | moo@0.5.2: 136 | resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} 137 | 138 | package-json-from-dist@1.0.1: 139 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 140 | 141 | path-key@3.1.1: 142 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 143 | engines: {node: '>=8'} 144 | 145 | path-scurry@2.0.0: 146 | resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} 147 | engines: {node: 20 || >=22} 148 | 149 | pow@file:../../pow/pkg: 150 | resolution: {directory: ../../pow/pkg, type: directory} 151 | 152 | safe-identifier@0.4.2: 153 | resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} 154 | 155 | shebang-command@2.0.0: 156 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 157 | engines: {node: '>=8'} 158 | 159 | shebang-regex@3.0.0: 160 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 161 | engines: {node: '>=8'} 162 | 163 | signal-exit@4.1.0: 164 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 165 | engines: {node: '>=14'} 166 | 167 | string-width@4.2.3: 168 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 169 | engines: {node: '>=8'} 170 | 171 | string-width@5.1.2: 172 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 173 | engines: {node: '>=12'} 174 | 175 | strip-ansi@6.0.1: 176 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 177 | engines: {node: '>=8'} 178 | 179 | strip-ansi@7.1.0: 180 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 181 | engines: {node: '>=12'} 182 | 183 | which@2.0.2: 184 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 185 | engines: {node: '>= 8'} 186 | hasBin: true 187 | 188 | wrap-ansi@7.0.0: 189 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 190 | engines: {node: '>=10'} 191 | 192 | wrap-ansi@8.1.0: 193 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 194 | engines: {node: '>=12'} 195 | 196 | yaml@2.7.1: 197 | resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} 198 | engines: {node: '>= 14'} 199 | hasBin: true 200 | 201 | snapshots: 202 | 203 | '@isaacs/cliui@8.0.2': 204 | dependencies: 205 | string-width: 5.1.2 206 | string-width-cjs: string-width@4.2.3 207 | strip-ansi: 7.1.0 208 | strip-ansi-cjs: strip-ansi@6.0.1 209 | wrap-ansi: 8.1.0 210 | wrap-ansi-cjs: wrap-ansi@7.0.0 211 | 212 | '@messageformat/convert@1.0.0': 213 | dependencies: 214 | make-plural: 6.2.2 215 | 216 | '@messageformat/core@3.4.0': 217 | dependencies: 218 | '@messageformat/date-skeleton': 1.1.0 219 | '@messageformat/number-skeleton': 1.2.0 220 | '@messageformat/parser': 5.1.1 221 | '@messageformat/runtime': 3.0.1 222 | make-plural: 7.4.0 223 | safe-identifier: 0.4.2 224 | 225 | '@messageformat/date-skeleton@1.1.0': {} 226 | 227 | '@messageformat/number-skeleton@1.2.0': {} 228 | 229 | '@messageformat/parser@5.1.1': 230 | dependencies: 231 | moo: 0.5.2 232 | 233 | '@messageformat/runtime@3.0.1': 234 | dependencies: 235 | make-plural: 7.4.0 236 | 237 | ansi-regex@5.0.1: {} 238 | 239 | ansi-regex@6.1.0: {} 240 | 241 | ansi-styles@4.3.0: 242 | dependencies: 243 | color-convert: 2.0.1 244 | 245 | ansi-styles@6.2.1: {} 246 | 247 | balanced-match@1.0.2: {} 248 | 249 | brace-expansion@2.0.1: 250 | dependencies: 251 | balanced-match: 1.0.2 252 | 253 | color-convert@2.0.1: 254 | dependencies: 255 | color-name: 1.1.4 256 | 257 | color-name@1.1.4: {} 258 | 259 | cross-spawn@7.0.6: 260 | dependencies: 261 | path-key: 3.1.1 262 | shebang-command: 2.0.0 263 | which: 2.0.2 264 | 265 | eastasianwidth@0.2.0: {} 266 | 267 | emoji-regex@8.0.0: {} 268 | 269 | emoji-regex@9.2.2: {} 270 | 271 | foreground-child@3.3.1: 272 | dependencies: 273 | cross-spawn: 7.0.6 274 | signal-exit: 4.1.0 275 | 276 | glob@11.0.2: 277 | dependencies: 278 | foreground-child: 3.3.1 279 | jackspeak: 4.1.0 280 | minimatch: 10.0.1 281 | minipass: 7.1.2 282 | package-json-from-dist: 1.0.1 283 | path-scurry: 2.0.0 284 | 285 | is-fullwidth-code-point@3.0.0: {} 286 | 287 | isexe@2.0.0: {} 288 | 289 | jackspeak@4.1.0: 290 | dependencies: 291 | '@isaacs/cliui': 8.0.2 292 | 293 | lru-cache@11.1.0: {} 294 | 295 | make-plural@6.2.2: {} 296 | 297 | make-plural@7.4.0: {} 298 | 299 | minimatch@10.0.1: 300 | dependencies: 301 | brace-expansion: 2.0.1 302 | 303 | minipass@7.1.2: {} 304 | 305 | moo@0.5.2: {} 306 | 307 | package-json-from-dist@1.0.1: {} 308 | 309 | path-key@3.1.1: {} 310 | 311 | path-scurry@2.0.0: 312 | dependencies: 313 | lru-cache: 11.1.0 314 | minipass: 7.1.2 315 | 316 | pow@file:../../pow/pkg: {} 317 | 318 | safe-identifier@0.4.2: {} 319 | 320 | shebang-command@2.0.0: 321 | dependencies: 322 | shebang-regex: 3.0.0 323 | 324 | shebang-regex@3.0.0: {} 325 | 326 | signal-exit@4.1.0: {} 327 | 328 | string-width@4.2.3: 329 | dependencies: 330 | emoji-regex: 8.0.0 331 | is-fullwidth-code-point: 3.0.0 332 | strip-ansi: 6.0.1 333 | 334 | string-width@5.1.2: 335 | dependencies: 336 | eastasianwidth: 0.2.0 337 | emoji-regex: 9.2.2 338 | strip-ansi: 7.1.0 339 | 340 | strip-ansi@6.0.1: 341 | dependencies: 342 | ansi-regex: 5.0.1 343 | 344 | strip-ansi@7.1.0: 345 | dependencies: 346 | ansi-regex: 6.1.0 347 | 348 | which@2.0.2: 349 | dependencies: 350 | isexe: 2.0.0 351 | 352 | wrap-ansi@7.0.0: 353 | dependencies: 354 | ansi-styles: 4.3.0 355 | string-width: 4.2.3 356 | strip-ansi: 6.0.1 357 | 358 | wrap-ansi@8.1.0: 359 | dependencies: 360 | ansi-styles: 6.2.1 361 | string-width: 5.1.2 362 | strip-ansi: 7.1.0 363 | 364 | yaml@2.7.1: {} 365 | -------------------------------------------------------------------------------- /web/js/pow.mjs: -------------------------------------------------------------------------------- 1 | // This file contains code adapted from https://github.com/TecharoHQ/anubis under the MIT License. 2 | import wasm from 'url:pow-wasm/pow_bg.wasm'; 3 | 4 | export default async function process( 5 | data, 6 | difficulty = 5, 7 | signal = null, 8 | progressCallback = null, 9 | threads = (navigator.hardwareConcurrency || 1), 10 | ) { 11 | return new Promise((resolve, reject) => { 12 | console.debug("fast algo"); 13 | const workers = []; 14 | const terminate = () => { 15 | workers.forEach((w) => w.terminate()); 16 | if (signal !== null) { 17 | // clean up listener to avoid memory leak 18 | signal.removeEventListener("abort", terminate); 19 | if (signal.aborted) { 20 | console.log("PoW aborted"); 21 | reject(new Error("PoW aborted")); 22 | } 23 | } 24 | }; 25 | if (signal !== null) { 26 | signal.addEventListener("abort", terminate, { once: true }); 27 | } 28 | 29 | (async () => { 30 | const wasmModule = await (await fetch(wasm)).arrayBuffer(); 31 | 32 | for (let i = 0; i < threads; i++) { 33 | let worker = new Worker(new URL("./pow.worker.js", import.meta.url), { type: "module" }); 34 | 35 | worker.onmessage = (event) => { 36 | if (typeof event.data === "number") { 37 | progressCallback?.(event.data); 38 | } else { 39 | terminate(); 40 | resolve(event.data); 41 | } 42 | }; 43 | 44 | worker.onerror = (event) => { 45 | terminate(); 46 | reject(event); 47 | }; 48 | 49 | worker.postMessage({ 50 | wasmModule, 51 | data, 52 | difficulty, 53 | nonce: i, 54 | threads, 55 | }); 56 | 57 | workers.push(worker); 58 | } 59 | })(); 60 | }); 61 | } -------------------------------------------------------------------------------- /web/js/pow.worker.js: -------------------------------------------------------------------------------- 1 | import { initSync, process_task } from "pow-wasm"; 2 | 3 | addEventListener('message', async (event) => { 4 | initSync({ module: event.data.wasmModule }); 5 | process_task(event.data.data, event.data.difficulty, event.data.nonce, event.data.threads); 6 | }); 7 | --------------------------------------------------------------------------------