diff --git a/.github/workflows/build-website-staging.yml b/.github/workflows/build-website-staging.yml index ff0a764c5..41cb4b6a1 100644 --- a/.github/workflows/build-website-staging.yml +++ b/.github/workflows/build-website-staging.yml @@ -16,7 +16,7 @@ steps: # Make sure we have some code to test - name: Harden runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: block allowed-endpoints: > @@ -29,14 +29,14 @@ runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 name: Install pnpm with: version: 10.0.0 run_install: false - name: Install Node.js - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 20.18.2 - name: Build diff --git a/.github/workflows/build-website.yml b/.github/workflows/build-website.yml index e6d83bdca..f99f10153 100644 --- a/.github/workflows/build-website.yml +++ b/.github/workflows/build-website.yml @@ -14,7 +14,7 @@ steps: # Make sure we have some code to test - name: Harden runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: block allowed-endpoints: > @@ -27,7 +27,7 @@ runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 @@ -37,7 +37,7 @@ run_install: false - name: Install Node.js - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 20.18.2 - name: Build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8a3488360..9fc0e9f73 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,11 +45,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v2.2.9 + uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v2.2.9 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v2.2.9 + uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v2.2.9 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -72,6 +72,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v2.2.9 + uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v2.2.9 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/copi-build.yml b/.github/workflows/copi-build.yml index c5f733a4b..55889fa4c 100644 --- a/.github/workflows/copi-build.yml +++ b/.github/workflows/copi-build.yml @@ -26,7 +26,7 @@ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} - name: build project @@ -34,7 +34,7 @@ run: docker build -f ./Dockerfile . - name: Cache deps id: cache-deps - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 env: cache-name: cache-elixir-deps with: diff --git a/.github/workflows/copi-deploy-production.yml b/.github/workflows/copi-deploy-production.yml index b90d2ccde..7d51caaa7 100644 --- a/.github/workflows/copi-deploy-production.yml +++ b/.github/workflows/copi-deploy-production.yml @@ -10,7 +10,7 @@ deploy-to-prod: runs-on: ubuntu-latest # Or another supported runner steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Install Elixir and Erlang uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4 with: diff --git a/.github/workflows/copi-deploy-staging.yml b/.github/workflows/copi-deploy-staging.yml index 8627e4767..331002467 100644 --- a/.github/workflows/copi-deploy-staging.yml +++ b/.github/workflows/copi-deploy-staging.yml @@ -28,10 +28,10 @@ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Cache deps id: cache-deps - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 env: cache-name: cache-elixir-deps with: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 02f43f046..07dc350f2 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -21,7 +21,7 @@ jobs: needs: hardening steps: - name: 'Checkout Repository' - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: 'Dependency Review' uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 with: diff --git a/.github/workflows/deploy-website-production.yml b/.github/workflows/deploy-website-production.yml index 58ad351bc..021ed591c 100644 --- a/.github/workflows/deploy-website-production.yml +++ b/.github/workflows/deploy-website-production.yml @@ -13,7 +13,7 @@ steps: # Make sure we have some code to test - name: Harden runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: block allowed-endpoints: > @@ -26,7 +26,7 @@ runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 name: Install pnpm with: @@ -34,7 +34,7 @@ run_install: false - name: Install Node.js - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: 20.18.2 - name: Build diff --git a/.github/workflows/deploy-website-staging.yml b/.github/workflows/deploy-website-staging.yml index 83f1319a1..140a9e524 100644 --- a/.github/workflows/deploy-website-staging.yml +++ b/.github/workflows/deploy-website-staging.yml @@ -19,7 +19,7 @@ steps: # Make sure we have some code to test - name: Harden runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: block allowed-endpoints: > @@ -33,7 +33,7 @@ needs: hardening steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Download a single artifact uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: diff --git a/.github/workflows/hardening.yaml b/.github/workflows/hardening.yaml index e2ab2dd57..bf73ca907 100644 --- a/.github/workflows/hardening.yaml +++ b/.github/workflows/hardening.yaml @@ -11,7 +11,7 @@ jobs: steps: # Make sure we have some code to test - name: Harden runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: block allowed-endpoints: > diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 739694404..ef483e020 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -18,7 +18,7 @@ runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Set the pip environment up - name: Get Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 132b2a26d..146eceef9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ runs-on: "ubuntu-latest" steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Set the pip environment up - name: Get Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 diff --git a/.github/workflows/run-tests-generate-output.yaml b/.github/workflows/run-tests-generate-output.yaml index 6147f9440..ce4f6b767 100644 --- a/.github/workflows/run-tests-generate-output.yaml +++ b/.github/workflows/run-tests-generate-output.yaml @@ -31,7 +31,7 @@ jobs: artifact-url: ${{ steps.upload_artifact.outputs.artifact-url }} steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create tmp branch for artifacts and get parent and object ref @@ -45,7 +45,7 @@ jobs: echo "object_tree=`git write-tree`" >> "$GITHUB_ENV" git switch --orphan "tmp-$BRANCH_NAME-artifacts" - name: Checkout branch for pull request - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.ref }} # Set the pip environment up diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index a035f994e..fe21eee9f 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} # Set the pip environment up diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 1423b0170..76cba3b36 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -26,7 +26,7 @@ jobs: id-token: write steps: - name: "Checkout code" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false @@ -57,6 +57,6 @@ jobs: # required for Code scanning alerts - name: "Upload SARIF results to code scanning" - uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 with: sarif_file: results.sarif diff --git a/Dockerfile b/Dockerfile index 762efaece..079bc6935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,5 +34,5 @@ COPY --chown=builder:union Pipfile Pipfile.lock ./ RUN pipenv --python "$(which python)" install --ignore-pipfile --dev ENTRYPOINT ["/usr/local/bin/pipenv"] -FROM mvdan/shfmt@sha256:20597e9d127ea8442384d0b2d8b755ae14e7aab29ad27ba8cc9d3440e7926e4d AS shfmt +FROM mvdan/shfmt@sha256:e414177e424692cd21a5113216edeeeb56fc76b0ed2e5eb3a6c48404d5548a76 AS shfmt ENTRYPOINT ["/bin/shfmt"] \ No newline at end of file diff --git a/Pipfile b/Pipfile index 1e3ad856a..bdf79fd0a 100644 --- a/Pipfile +++ b/Pipfile @@ -4,12 +4,12 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "==25.11.0" -coverage = "==7.12.0" +black = "==25.12.0" +coverage = "==7.13.0" flake8 = "==7.3.0" httpretty = "==1.1.4" mypy = "==1.19.0" -pytest = "==9.0.1" +pytest = "==9.0.2" pytest-cov = "==7.0.0" freezegun = "==1.5.5" security = "==1.3.1" @@ -21,7 +21,7 @@ qrcode = "==8.2" requests = "==2.32.5" types-requests = "==2.32.4.20250913" typing_extensions = "==4.8.0" -urllib3 = "==2.5.0" +urllib3 = "==2.6.2" charset-normalizer = "==3.4.4" python-docx = "==1.1.0" PyYAML = "==6.0.1" diff --git a/Pipfile.lock b/Pipfile.lock index 5e9e4501b..cbf09a617 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5b26c1c401070186793f696471305b12e18f0e5fc565b83bd44aa672926ed988" + "sha256": "67bc4f529a7a81dd8dd0c7a99d98f5e2172e687b0f9adf7a0d850288a4e28ec3" }, "pipfile-spec": 6, "requires": { @@ -645,47 +645,48 @@ }, "urllib3": { "hashes": [ - "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", - "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", + "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.5.0" + "version": "==2.6.2" } }, "develop": { "black": { "hashes": [ - "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", - "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", - "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", - "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", - "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", - "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", - "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", - "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", - "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", - "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", - "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", - "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", - "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", - "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", - "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", - "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", - "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", - "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", - "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", - "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", - "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", - "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", - "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", - "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", - "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", - "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03" + "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", + "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f", + "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", + "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", + "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", + "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea", + "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", + "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", + "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", + "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", + "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", + "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", + "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", + "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a", + "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", + "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", + "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", + "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", + "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", + "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", + "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", + "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da", + "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", + "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", + "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", + "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", + "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==25.11.0" + "markers": "python_version >= '3.10'", + "version": "==25.12.0" }, "certifi": { "hashes": [ @@ -808,112 +809,112 @@ }, "click": { "hashes": [ - "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", - "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" ], "markers": "python_version >= '3.10'", - "version": "==8.3.0" + "version": "==8.3.1" }, "coverage": { "extras": [ "toml" ], "hashes": [ - "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", - "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", - "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", - "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", - "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", - "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", - "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", - "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", - "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", - "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", - "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", - "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", - "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", - "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", - "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", - "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", - "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", - "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", - "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", - "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", - "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", - "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", - "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", - "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", - "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", - "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", - "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", - "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", - "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", - "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", - "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", - "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", - "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", - "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", - "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", - "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", - "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", - "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", - "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", - "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", - "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", - "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", - "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", - "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", - "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", - "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", - "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", - "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", - "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", - "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", - "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", - "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", - "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", - "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", - "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", - "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", - "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", - "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", - "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", - "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", - "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", - "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", - "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", - "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", - "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", - "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", - "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", - "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", - "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", - "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", - "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", - "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", - "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", - "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", - "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", - "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", - "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", - "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", - "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", - "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", - "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", - "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", - "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", - "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", - "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", - "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", - "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", - "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", - "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", - "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", - "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", - "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d" + "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", + "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", + "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", + "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", + "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", + "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", + "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", + "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", + "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", + "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", + "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", + "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", + "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", + "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", + "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", + "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", + "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", + "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", + "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", + "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", + "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", + "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", + "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", + "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", + "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", + "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", + "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", + "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", + "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", + "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", + "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", + "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", + "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", + "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", + "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", + "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", + "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", + "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", + "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", + "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", + "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", + "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", + "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", + "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", + "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", + "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", + "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", + "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", + "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", + "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", + "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", + "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", + "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", + "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", + "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", + "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", + "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", + "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", + "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", + "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", + "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", + "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", + "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", + "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", + "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", + "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", + "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", + "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", + "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", + "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", + "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", + "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", + "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", + "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", + "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", + "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", + "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", + "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", + "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", + "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", + "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", + "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", + "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", + "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", + "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", + "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", + "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", + "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", + "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", + "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", + "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", + "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6" ], "markers": "python_version >= '3.10'", - "version": "==7.12.0" + "version": "==7.13.0" }, "exceptiongroup": { "hashes": [ @@ -1037,11 +1038,11 @@ }, "platformdirs": { "hashes": [ - "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", - "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3" + "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", + "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31" ], "markers": "python_version >= '3.10'", - "version": "==4.5.0" + "version": "==4.5.1" }, "pluggy": { "hashes": [ @@ -1077,12 +1078,12 @@ }, "pytest": { "hashes": [ - "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", - "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad" + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==9.0.1" + "version": "==9.0.2" }, "pytest-cov": { "hashes": [ diff --git a/RATE_LIMITING_IMPLEMENTATION.md b/RATE_LIMITING_IMPLEMENTATION.md new file mode 100644 index 000000000..d3bd75112 --- /dev/null +++ b/RATE_LIMITING_IMPLEMENTATION.md @@ -0,0 +1,108 @@ +# Rate Limiting Implementation for Player Creation + +## Overview +This implementation addresses GitHub Issue #1877 by adding IP-based rate limiting for player creation, separate from the existing game creation rate limit, to protect against CAPEC 212 (Functionality Misuse) attacks. + +## Changes Made + +### 1. Rate Limiter Module (`lib/copi/rate_limiter.ex`) + +**Added player_creation action:** +- Updated `check_rate/2` to accept `:player_creation` action +- Updated `record_action/2` to accept `:player_creation` action +- Added player creation configuration with default limits: + - Maximum players per IP: 20 + - Time window: 3600 seconds (1 hour) + +**Configuration:** +```elixir +player_creation: %{ + max_requests: get_env(:max_players_per_ip, 20), + window_seconds: get_env(:player_creation_window_seconds, 3600) +} +``` + +### 2. Player Form Component (`lib/copi_web/live/player_live/form_component.ex`) + +**Added rate limiting to player creation:** +- Added `get_connect_ip/1` helper function to extract IP address from socket +- Modified `save_player/3` for `:new` action to: + 1. Get the connecting IP address + 2. Check rate limit before creating player + 3. Only create player if rate limit is not exceeded + 4. Record the action after successful creation + 5. Display user-friendly error message if rate limited + +**Error message shown to users:** +``` +"Rate limit exceeded. Too many players created from your IP address. +Please try again in X seconds. This limit helps ensure service +availability for all users." +``` + +### 3. Tests (`test/copi/rate_limiter_test.exs`) + +**Added comprehensive test coverage:** +- Test that player creation is allowed under the limit +- Test that player creation is blocked when limit is exceeded +- Test that player creation limit is separate from game creation limit +- Updated configuration test to verify player_creation config exists + +## Key Features + +### Separate Limits +The player creation rate limit is maintained **separately** from the game creation rate limit. This means: +- An IP that has exhausted its game creation quota can still create players +- An IP that has exhausted its player creation quota can still create games +- Each limit is tracked independently in the GenServer state + +### Configurable Limits +The limits can be configured via application environment: +```elixir +config :copi, Copi.RateLimiter, + max_players_per_ip: 20, + player_creation_window_seconds: 3600 +``` + +### User-Friendly Error Handling +When rate limited, users receive: +- Clear explanation of why they were blocked +- Time until they can try again (retry_after in seconds) +- Explanation that this protects service availability + +## Security Benefits + +1. **CAPEC 212 Mitigation**: Prevents functionality misuse by limiting the rate of player creation from a single IP +2. **DoS Protection**: Helps maintain service availability under attack +3. **Resource Conservation**: Prevents database and system resource exhaustion +4. **Granular Control**: Separate limits allow fine-tuned protection for different actions + +## Default Limits Summary + +| Action | Max Requests | Time Window | +|--------|--------------|-------------| +| Game Creation | 10 | 1 hour | +| Player Creation | 20 | 1 hour | +| Connection | 50 | 5 minutes | + +## Testing + +Run the test suite: +```bash +mix test test/copi/rate_limiter_test.exs +``` + +All tests should pass, including the new player creation rate limiting tests. + +## Future Enhancements + +As mentioned in the issue, if this is still insufficient, the next step would be: +- Implement authentication +- Associate rate limits with user accounts in addition to IP addresses +- Track browser fingerprints along with IP addresses + +--- + +**Issue Reference:** OWASP/cornucopia#1877 +**Related Security Control:** CAPEC-212 (Functionality Misuse) +**Implemented by:** @immortal71 diff --git a/copi.owasp.org/Dockerfile b/copi.owasp.org/Dockerfile index 71d1d4ec6..3d88c0990 100644 --- a/copi.owasp.org/Dockerfile +++ b/copi.owasp.org/Dockerfile @@ -12,7 +12,7 @@ # - https://pkgs.org/ - resource for finding needed packages # - Ex: hexpm/elixir:1.14.2-erlang-25.1.1-debian-bullseye-20220801-slim # -FROM --platform=linux/amd64 hexpm/elixir:1.19-erlang-28.2-debian-bullseye-20251117@sha256:63067931e21db6d893bef21f54dda18df09b6e65a793616d7b07246eb11e4904 as builder +FROM --platform=linux/amd64 hexpm/elixir:1.19-erlang-28.3-debian-bullseye-20251208@sha256:9d1e59c326674de89a2eac9cd7f118ae2917e1c6cde02e8fa4cd785198ca9be0 as builder # install build dependencies RUN apt-get update -y && apt-get install -y build-essential git nodejs npm \ && apt-get clean && rm -f /var/lib/apt/lists/*_* @@ -58,7 +58,7 @@ RUN mix release # start a new build stage so that the final image will only contain # the compiled release and other runtime necessities -FROM --platform=linux/amd64 hexpm/elixir:1.19-erlang-28.2-debian-bullseye-20251117@sha256:63067931e21db6d893bef21f54dda18df09b6e65a793616d7b07246eb11e4904 +FROM --platform=linux/amd64 hexpm/elixir:1.19-erlang-28.3-debian-bullseye-20251208@sha256:9d1e59c326674de89a2eac9cd7f118ae2917e1c6cde02e8fa4cd785198ca9be0 RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ && apt-get clean && rm -f /var/lib/apt/lists/*_* diff --git a/copi.owasp.org/README.md b/copi.owasp.org/README.md index 8a1a64d3a..ca283c1e1 100644 --- a/copi.owasp.org/README.md +++ b/copi.owasp.org/README.md @@ -75,6 +75,52 @@ To start your Phoenix server: Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +## Security Features + +### Rate Limiting + +Copi implements IP-based rate limiting to protect against abuse and ensure availability for all users. This addresses CAPEC 212 (Functionality Misuse) attacks. + +**Features:** +- **Game Creation Limiting**: Limits the number of games that can be created from a single IP address +- **Connection Limiting**: Limits the number of WebSocket connections from a single IP address +- **Configurable Limits**: All limits and time windows are configurable via environment variables + +**Configuration:** + +Set the following environment variables to customize rate limits: + +```bash +# Maximum games per IP (default: 10) +export MAX_GAMES_PER_IP=10 + +# Time window for game creation in seconds (default: 3600 = 1 hour) +export GAME_CREATION_WINDOW_SECONDS=3600 + +# Maximum connections per IP (default: 50) +export MAX_CONNECTIONS_PER_IP=50 + +# Time window for connections in seconds (default: 300 = 5 minutes) +export CONNECTION_WINDOW_SECONDS=300 +``` + +**How it works:** +- The rate limiter tracks requests by IP address +- When a limit is exceeded, users receive a clear error message with a retry time +- Expired entries are automatically cleaned up every 5 minutes +- Rate limits are independent for game creation vs. connections +- The system handles both IPv4 and IPv6 addresses +- X-Forwarded-For headers are respected for reverse proxy deployments + +**For Reverse Proxy Deployments:** + +If deploying behind a reverse proxy (nginx, Apache, Cloudflare, etc.), ensure the proxy passes the real client IP: + +```nginx +# Nginx example +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). ## More about Phoenix @@ -97,6 +143,14 @@ Login to fly and create a PostgreSQL cluster. See: see: https://fly.io/dashboard fly launch --no-deploy Make a note of the host and name of the app and the name of the postgresql cluster. + +Configure rate limiting (optional, uses defaults if not set): + + fly secrets set MAX_GAMES_PER_IP=10 --app + fly secrets set GAME_CREATION_WINDOW_SECONDS=3600 --app + fly secrets set MAX_CONNECTIONS_PER_IP=50 --app + fly secrets set CONNECTION_WINDOW_SECONDS=300 --app + Then deploy the app from `./copi.owasp.org` fly mpg attach --app @@ -139,6 +193,12 @@ Set your prefered app name instead of `` heroku config:set SECRET_KEY_BASE=$(mix phx.gen.secret) heroku config:set POOL_SIZE=18 heroku config:set PROJECT_PATH=copi.owasp.org # points to the subdirectory + + # Optional: Configure rate limiting (uses defaults if not set) + heroku config:set MAX_GAMES_PER_IP=10 + heroku config:set GAME_CREATION_WINDOW_SECONDS=3600 + heroku config:set MAX_CONNECTIONS_PER_IP=50 + heroku config:set CONNECTION_WINDOW_SECONDS=300 ### Heroku deploy @@ -164,4 +224,15 @@ Reconfigure the apps host address Setup SSL on for your dns provider e.g: https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/ # PEM format - heroku certs:add cloudflare.crt cloudflare.key \ No newline at end of file + heroku certs:add cloudflare.crt cloudflare.key + +## Other useful things + +### Statistics + +Get the number of games and users per month. We do not track users. We only look at totals per month. + +SELECT EXTRACT(YEAR FROM created_at) AS year, EXTRACT(MONTH FROM created_at) AS month, count(*) AS games_count FROM games WHERE created_at is not null group by year, month ORDER by year ASC, month ASC; + + +SELECT EXTRACT(YEAR FROM inserted_at) AS year, EXTRACT(MONTH FROM inserted_at) AS month, count(*) AS players_count FROM players WHERE inserted_at is not null group by year, month ORDER by year ASC, month ASC; diff --git a/copi.owasp.org/SECURITY.md b/copi.owasp.org/SECURITY.md new file mode 100644 index 000000000..0ea6d7733 --- /dev/null +++ b/copi.owasp.org/SECURITY.md @@ -0,0 +1,203 @@ +# Security Implementation for Copi + +This document describes the security measures implemented in Copi to protect against abuse and ensure service availability. + +## Overview + +Copi has implemented IP-based rate limiting to protect against **CAPEC 212 (Functionality Misuse)** attacks. These protections help ensure that the service remains available for all legitimate users by preventing a single source from overwhelming the system. + +## Implemented Protections + +### 1. Game Creation Rate Limiting + +**Purpose**: Prevents a single IP address from creating an excessive number of games, which could lead to database exhaustion or denial of service. + +**Default Configuration**: +- Maximum games per IP: 10 +- Time window: 3600 seconds (1 hour) + +**Behavior**: +- Tracks game creation attempts by IP address +- When the limit is exceeded, users receive a clear error message +- The limit resets after the time window expires +- Different IP addresses have independent limits + +**Configuration**: +```bash +export MAX_GAMES_PER_IP=10 +export GAME_CREATION_WINDOW_SECONDS=3600 +``` + +### 2. WebSocket Connection Rate Limiting + +**Purpose**: Prevents a single IP address from opening excessive WebSocket connections, which could exhaust server resources. + +**Default Configuration**: +- Maximum connections per IP: 50 +- Time window: 300 seconds (5 minutes) + +**Behavior**: +- Tracks connection attempts by IP address +- When the limit is exceeded, connections are rejected with an error message +- The limit resets after the time window expires +- Does not affect existing active connections + +**Configuration**: +```bash +export MAX_CONNECTIONS_PER_IP=50 +export CONNECTION_WINDOW_SECONDS=300 +``` + +## Technical Implementation + +### Architecture + +The rate limiting system consists of several components: + +1. **Copi.RateLimiter** (`lib/copi/rate_limiter.ex`) + - GenServer that maintains rate limit state + - Tracks requests per IP address and action type + - Automatically cleans up expired entries every 5 minutes + - Provides a simple API for checking and recording actions + +2. **CopiWeb.Plugs.RateLimiter** (`lib/copi_web/plugs/rate_limiter.ex`) + - Plug for HTTP request rate limiting + - Extracts IP addresses from connections + - Handles X-Forwarded-For headers for reverse proxies + - Returns proper HTTP 429 status codes when limits are exceeded + +3. **LiveView Integration** + - Game creation rate limiting in `CopiWeb.GameLive.CreateGameForm` + - Connection rate limiting in `CopiWeb.GameLive.Index` + - Provides user-friendly error messages + +### IP Address Handling + +The system correctly handles: +- IPv4 addresses (e.g., 192.168.1.1) +- IPv6 addresses (e.g., 2001:db8::1) +- X-Forwarded-For headers (for reverse proxy deployments) +- Multiple IP addresses in X-Forwarded-For (uses the first one) + +### Rate Limit Response Headers + +When rate limiting is active, the following headers are included in responses: + +- `X-RateLimit-Remaining`: Number of requests remaining in the current window +- `Retry-After`: Seconds until the rate limit resets (only included when rate limited) + +### Error Messages + +Users who exceed rate limits receive clear, informative error messages: + +``` +Rate limit exceeded. Too many games created from your IP address. +Please try again in 3600 seconds. +This limit helps ensure service availability for all users. +``` + +## Deployment Considerations + +### Reverse Proxy Configuration + +If deploying behind a reverse proxy (nginx, HAProxy, Cloudflare, etc.), ensure the real client IP is passed through: + +**Nginx**: +```nginx +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + +**Apache**: +```apache +RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s" +``` + +**Cloudflare**: +Cloudflare automatically sets the CF-Connecting-IP header. The X-Forwarded-For header should also be present. + +### Monitoring + +Monitor the following metrics to detect abuse or adjust rate limits: + +- Rate limit rejections (logged as warnings) +- Rate limit state size (number of tracked IPs) +- 429 HTTP responses +- Flash messages about rate limiting + +### Adjusting Rate Limits + +Rate limits can be adjusted based on your deployment needs: + +**For Development/Testing**: +```bash +export MAX_GAMES_PER_IP=100 +export MAX_CONNECTIONS_PER_IP=200 +``` + +**For High-Traffic Production**: +```bash +export MAX_GAMES_PER_IP=5 +export GAME_CREATION_WINDOW_SECONDS=7200 # 2 hours +export MAX_CONNECTIONS_PER_IP=30 +export CONNECTION_WINDOW_SECONDS=600 # 10 minutes +``` + +**For Low-Traffic/Internal Use**: +```bash +export MAX_GAMES_PER_IP=50 +export MAX_CONNECTIONS_PER_IP=100 +``` + +## Testing + +The implementation includes comprehensive tests: + +- **Unit tests** for the RateLimiter GenServer (`test/copi/rate_limiter_test.exs`) +- **Integration tests** for the RateLimiter Plug (`test/copi_web/plugs/rate_limiter_test.exs`) + +Run tests: +```bash +mix test +``` + +Run specific rate limiter tests: +```bash +mix test test/copi/rate_limiter_test.exs +mix test test/copi_web/plugs/rate_limiter_test.exs +``` + +## Future Enhancements + +Potential future security enhancements (not currently implemented): + +1. **Authentication Integration** + - Associate rate limits with authenticated users + - Different limits for authenticated vs. anonymous users + - Per-user rate limiting instead of just per-IP + +2. **Geographic Rate Limiting** + - Different limits based on geographic location + - Blocking or stricter limits for high-risk regions + +3. **Adaptive Rate Limiting** + - Automatically adjust limits based on system load + - Stricter limits during high traffic periods + +4. **CAPTCHA Integration** + - Require CAPTCHA after repeated rate limit violations + - Optional CAPTCHA for game creation + +5. **Rate Limit Dashboard** + - Admin interface to view current rate limit state + - Ability to manually block/unblock IPs + - Real-time monitoring of rate limit violations + +## Reporting Security Issues + +If you discover a security vulnerability in Copi, please report it to the OWASP Cornucopia team through the [GitHub Security Advisories](https://github.com/OWASP/cornucopia/security/advisories) page. + +## References + +- [CAPEC-212: Functionality Misuse](https://capec.mitre.org/data/definitions/212.html) +- [OWASP API Security Top 10 - API4:2023 Unrestricted Resource Consumption](https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/) +- [Phoenix Framework Security](https://hexdocs.pm/phoenix/security.html) diff --git a/copi.owasp.org/config/config.exs b/copi.owasp.org/config/config.exs index 4e51421d4..a27935fda 100644 --- a/copi.owasp.org/config/config.exs +++ b/copi.owasp.org/config/config.exs @@ -27,6 +27,17 @@ config :copi, CopiWeb.Endpoint, config :copi, env: Mix.env() +# Configure rate limiting to prevent abuse (CAPEC 212 - Functionality Misuse) +config :copi, Copi.RateLimiter, + # Maximum number of games that can be created from a single IP in the time window + max_games_per_ip: System.get_env("MAX_GAMES_PER_IP", "10") |> String.to_integer(), + # Time window in seconds for game creation rate limiting (default: 1 hour) + game_creation_window_seconds: System.get_env("GAME_CREATION_WINDOW_SECONDS", "3600") |> String.to_integer(), + # Maximum number of WebSocket connections from a single IP in the time window + max_connections_per_ip: System.get_env("MAX_CONNECTIONS_PER_IP", "50") |> String.to_integer(), + # Time window in seconds for connection rate limiting (default: 5 minutes) + connection_window_seconds: System.get_env("CONNECTION_WINDOW_SECONDS", "300") |> String.to_integer() + # Configure tailwind (the version is required) config :tailwind, version: "3.4.0", diff --git a/copi.owasp.org/lib/copi/application.ex b/copi.owasp.org/lib/copi/application.ex index b6493a6a5..0d9ba8bf8 100644 --- a/copi.owasp.org/lib/copi/application.ex +++ b/copi.owasp.org/lib/copi/application.ex @@ -15,6 +15,8 @@ defmodule Copi.Application do {Phoenix.PubSub, name: Copi.PubSub}, # Start the DNS clustering {DNSCluster, query: Application.get_env(:copi, :dns_cluster_query) || :ignore}, + # Start the Rate Limiter for security + Copi.RateLimiter, # Start the Endpoint (http/https) CopiWeb.Endpoint # Start a worker by calling: Copi.Worker.start_link(arg) diff --git a/copi.owasp.org/lib/copi/rate_limiter.ex b/copi.owasp.org/lib/copi/rate_limiter.ex new file mode 100644 index 000000000..44a67fbd9 --- /dev/null +++ b/copi.owasp.org/lib/copi/rate_limiter.ex @@ -0,0 +1,184 @@ +defmodule Copi.RateLimiter do + @moduledoc """ + Rate limiter to prevent abuse by limiting requests per IP address. + + This module implements rate limiting for game creation, player creation, and user connections + to protect against CAPEC 212 (Functionality Misuse) attacks. + """ + + use GenServer + require Logger + + @cleanup_interval :timer.minutes(5) + + # Client API + + @doc """ + Starts the rate limiter GenServer. + """ + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @doc """ + Checks if a request from the given IP for the specified action should be allowed. + + Returns `{:ok, remaining}` if allowed, `{:error, :rate_limited, retry_after}` if blocked. + """ + def check_rate(ip_address, action) when action in [:game_creation, :player_creation, :connection] do + GenServer.call(__MODULE__, {:check_rate, ip_address, action}) + end + + @doc """ + Records a successful action for rate limiting tracking. + """ + def record_action(ip_address, action) when action in [:game_creation, :player_creation, :connection] do + GenServer.cast(__MODULE__, {:record_action, ip_address, action}) + end + + @doc """ + Clears all rate limit data for an IP address (useful for testing). + """ + def clear_ip(ip_address) do + GenServer.cast(__MODULE__, {:clear_ip, ip_address}) + end + + @doc """ + Gets current rate limit configuration. + """ + def get_config do + GenServer.call(__MODULE__, :get_config) + end + + # Server callbacks + + @impl true + def init(_opts) do + # Schedule periodic cleanup + schedule_cleanup() + + config = %{ + game_creation: %{ + max_requests: get_env(:max_games_per_ip, 10), + window_seconds: get_env(:game_creation_window_seconds, 3600) + }, + player_creation: %{ + max_requests: get_env(:max_players_per_ip, 20), + window_seconds: get_env(:player_creation_window_seconds, 3600) + }, + connection: %{ + max_requests: get_env(:max_connections_per_ip, 50), + window_seconds: get_env(:connection_window_seconds, 300) + } + } + + state = %{ + requests: %{}, + config: config + } + + Logger.info("RateLimiter started with config: #{inspect(config)}") + + {:ok, state} + end + + @impl true + def handle_call({:check_rate, ip_address, action}, _from, state) do + now = System.system_time(:second) + config = state.config[action] + + ip_requests = get_ip_requests(state, ip_address, action) + + # Filter out expired requests + valid_requests = Enum.filter(ip_requests, fn timestamp -> + now - timestamp < config.window_seconds + end) + + count = length(valid_requests) + remaining = max(0, config.max_requests - count) + + if count < config.max_requests do + {:reply, {:ok, remaining}, state} + else + oldest_request = List.first(valid_requests) + retry_after = oldest_request + config.window_seconds - now + + Logger.warning( + "Rate limit exceeded for IP #{inspect(ip_address)}, action: #{action}, " <> + "count: #{count}/#{config.max_requests}, retry_after: #{retry_after}s" + ) + + {:reply, {:error, :rate_limited, retry_after}, state} + end + end + + @impl true + def handle_call(:get_config, _from, state) do + {:reply, state.config, state} + end + + @impl true + def handle_cast({:record_action, ip_address, action}, state) do + now = System.system_time(:second) + + ip_requests = get_ip_requests(state, ip_address, action) + updated_requests = [now | ip_requests] + + new_requests = Map.update( + state.requests, + ip_address, + %{action => updated_requests}, + fn actions -> Map.put(actions, action, updated_requests) end + ) + + {:noreply, %{state | requests: new_requests}} + end + + @impl true + def handle_cast({:clear_ip, ip_address}, state) do + new_requests = Map.delete(state.requests, ip_address) + {:noreply, %{state | requests: new_requests}} + end + + @impl true + def handle_info(:cleanup, state) do + now = System.system_time(:second) + + cleaned_requests = state.requests + |> Enum.map(fn {ip, actions} -> + cleaned_actions = actions + |> Enum.map(fn {action, timestamps} -> + config = state.config[action] + valid_timestamps = Enum.filter(timestamps, fn timestamp -> + now - timestamp < config.window_seconds + end) + {action, valid_timestamps} + end) + |> Enum.filter(fn {_action, timestamps} -> length(timestamps) > 0 end) + |> Map.new() + + {ip, cleaned_actions} + end) + |> Enum.filter(fn {_ip, actions} -> map_size(actions) > 0 end) + |> Map.new() + + schedule_cleanup() + + {:noreply, %{state | requests: cleaned_requests}} + end + + # Private helpers + + defp get_ip_requests(state, ip_address, action) do + get_in(state.requests, [ip_address, action]) || [] + end + + defp schedule_cleanup do + Process.send_after(self(), :cleanup, @cleanup_interval) + end + + defp get_env(key, default) do + Application.get_env(:copi, __MODULE__, []) + |> Keyword.get(key, default) + end +end diff --git a/copi.owasp.org/lib/copi_web/endpoint.ex b/copi.owasp.org/lib/copi_web/endpoint.ex index 54c4eca5c..2d64f820d 100644 --- a/copi.owasp.org/lib/copi_web/endpoint.ex +++ b/copi.owasp.org/lib/copi_web/endpoint.ex @@ -12,8 +12,11 @@ defmodule CopiWeb.Endpoint do ] socket "/live", Phoenix.LiveView.Socket, - websocket: [timeout: 45_000, connect_info: [session: @session_options]], - longpoll: [connect_info: [session: @session_options]] + websocket: [ + timeout: 45_000, + connect_info: [session: @session_options, peer_data: true, x_headers: ["x-forwarded-for"]] + ], + longpoll: [connect_info: [session: @session_options, peer_data: true, x_headers: ["x-forwarded-for"]]] # Serve at "/" the static files from "priv/static" directory. # diff --git a/copi.owasp.org/lib/copi_web/live/game_live/create_game_form.ex b/copi.owasp.org/lib/copi_web/live/game_live/create_game_form.ex index de424483f..83ebd44e8 100644 --- a/copi.owasp.org/lib/copi_web/live/game_live/create_game_form.ex +++ b/copi.owasp.org/lib/copi_web/live/game_live/create_game_form.ex @@ -107,15 +107,45 @@ defmodule CopiWeb.GameLive.CreateGameForm do end defp save_game(socket, :new, game_params) do - case Cornucopia.create_game(game_params) do - {:ok, game} -> + # Get the IP address for rate limiting + ip_address = get_connect_ip(socket) + + # Check rate limit before creating game + case Copi.RateLimiter.check_rate(ip_address, :game_creation) do + {:ok, _remaining} -> + case Cornucopia.create_game(game_params) do + {:ok, game} -> + # Record the action after successful creation + Copi.RateLimiter.record_action(ip_address, :game_creation) + + {:noreply, + socket + |> put_flash(:info, "Game created successfully") + |> push_navigate(to: ~p"/games/#{game.id}")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + + {:error, :rate_limited, retry_after} -> {:noreply, socket - |> put_flash(:info, "Game created successfully") - |> push_navigate(to: ~p"/games/#{game.id}")} + |> put_flash( + :error, + "Rate limit exceeded. Too many games created from your IP address. " <> + "Please try again in #{retry_after} seconds. " <> + "This limit helps ensure service availability for all users." + ) + |> assign_form(socket.assigns.form.source)} + end + end - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign_form(socket, changeset)} + defp get_connect_ip(socket) do + case get_connect_info(socket, :peer_data) do + %{address: {a, b, c, d}} -> "#{a}.#{b}.#{c}.#{d}" + %{address: {a, b, c, d, e, f, g, h}} -> + :inet.ntoa({a, b, c, d, e, f, g, h}) |> to_string() + _ -> "unknown" end end end diff --git a/copi.owasp.org/lib/copi_web/live/game_live/index.ex b/copi.owasp.org/lib/copi_web/live/game_live/index.ex index 4724ae086..ef49ed0ae 100644 --- a/copi.owasp.org/lib/copi_web/live/game_live/index.ex +++ b/copi.owasp.org/lib/copi_web/live/game_live/index.ex @@ -7,7 +7,24 @@ defmodule CopiWeb.GameLive.Index do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, :games, nil)} + # Rate limit WebSocket connections + ip_address = get_connect_ip(socket) + + case Copi.RateLimiter.check_rate(ip_address, :connection) do + {:ok, _remaining} -> + Copi.RateLimiter.record_action(ip_address, :connection) + {:ok, assign(socket, :games, nil)} + + {:error, :rate_limited, retry_after} -> + {:ok, + socket + |> put_flash( + :error, + "Connection rate limit exceeded. Too many connections from your IP address. " <> + "Please try again in #{retry_after} seconds." + ) + |> assign(:games, nil)} + end end @impl true @@ -44,4 +61,13 @@ defmodule CopiWeb.GameLive.Index do {:noreply, assign(socket, :games, new_state)} end + defp get_connect_ip(socket) do + case get_connect_info(socket, :peer_data) do + %{address: {a, b, c, d}} -> "#{a}.#{b}.#{c}.#{d}" + %{address: {a, b, c, d, e, f, g, h}} -> + :inet.ntoa({a, b, c, d, e, f, g, h}) |> to_string() + _ -> "unknown" + end + end + end diff --git a/copi.owasp.org/lib/copi_web/live/player_live/form_component.ex b/copi.owasp.org/lib/copi_web/live/player_live/form_component.ex index 4e2f523f9..45587125f 100644 --- a/copi.owasp.org/lib/copi_web/live/player_live/form_component.ex +++ b/copi.owasp.org/lib/copi_web/live/player_live/form_component.ex @@ -74,23 +74,52 @@ defmodule CopiWeb.PlayerLive.FormComponent do end defp save_player(socket, :new, player_params) do - case Cornucopia.create_player(player_params) do - {:ok, player} -> - - {:ok, updated_game} = Cornucopia.Game.find(socket.assigns.player.game_id) - CopiWeb.Endpoint.broadcast(topic(updated_game.id), "game:updated", updated_game) - + # Get the IP address for rate limiting + ip_address = get_connect_ip(socket) + + # Check rate limit before creating player + case Copi.RateLimiter.check_rate(ip_address, :player_creation) do + {:ok, _remaining} -> + case Cornucopia.create_player(player_params) do + {:ok, player} -> + # Record the action after successful creation + Copi.RateLimiter.record_action(ip_address, :player_creation) + + {:ok, updated_game} = Cornucopia.Game.find(socket.assigns.player.game_id) + CopiWeb.Endpoint.broadcast(topic(updated_game.id), "game:updated", updated_game) + + {:noreply, + socket + |> assign(:game, updated_game) + |> push_navigate(to: ~p"/games/#{player.game_id}/players/#{player.id}")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign_form(socket, changeset)} + end + + {:error, :rate_limited, retry_after} -> {:noreply, socket - |> assign(:game, updated_game) - |> push_navigate(to: ~p"/games/#{player.game_id}/players/#{player.id}")} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign_form(socket, changeset)} + |> put_flash( + :error, + "Rate limit exceeded. Too many players created from your IP address. " <> + "Please try again in #{retry_after} seconds. " <> + "This limit helps ensure service availability for all users." + ) + |> assign_form(socket.assigns.form.source)} end end def topic(game_id) do "game:#{game_id}" end + + defp get_connect_ip(socket) do + case get_connect_info(socket, :peer_data) do + %{address: {a, b, c, d}} -> "#{a}.#{b}.#{c}.#{d}" + %{address: {a, b, c, d, e, f, g, h}} -> + "#{Integer.to_string(a, 16)}:#{Integer.to_string(b, 16)}:#{Integer.to_string(c, 16)}:#{Integer.to_string(d, 16)}:#{Integer.to_string(e, 16)}:#{Integer.to_string(f, 16)}:#{Integer.to_string(g, 16)}:#{Integer.to_string(h, 16)}" + _ -> "unknown" + end + end end diff --git a/copi.owasp.org/lib/copi_web/plugs/rate_limiter.ex b/copi.owasp.org/lib/copi_web/plugs/rate_limiter.ex new file mode 100644 index 000000000..b8b088b62 --- /dev/null +++ b/copi.owasp.org/lib/copi_web/plugs/rate_limiter.ex @@ -0,0 +1,59 @@ +defmodule CopiWeb.Plugs.RateLimiter do + @moduledoc """ + Plug for rate limiting HTTP requests based on IP address. + """ + + import Plug.Conn + require Logger + + alias Copi.RateLimiter + + def init(opts), do: opts + + def call(conn, opts) do + action = Keyword.get(opts, :action, :game_creation) + ip_address = get_ip_address(conn) + + case RateLimiter.check_rate(ip_address, action) do + {:ok, remaining} -> + RateLimiter.record_action(ip_address, action) + conn + |> put_resp_header("x-ratelimit-remaining", to_string(remaining)) + + {:error, :rate_limited, retry_after} -> + Logger.warning("Rate limit exceeded for IP: #{inspect(ip_address)}, action: #{action}") + + conn + |> put_resp_header("retry-after", to_string(retry_after)) + |> put_resp_header("x-ratelimit-remaining", "0") + |> send_resp(429, rate_limit_message(action, retry_after)) + |> halt() + end + end + + defp get_ip_address(conn) do + # Always use conn.remote_ip, which is set by Plug.RemoteIp if present in the plug pipeline. + # Ensure Plug.RemoteIp is used with a properly configured :proxies list for safe client IP extraction. + case conn.remote_ip do + {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}" + {a, b, c, d, e, f, g, h} -> + "#{Integer.to_string(a, 16)}:#{Integer.to_string(b, 16)}:#{Integer.to_string(c, 16)}:#{Integer.to_string(d, 16)}:#{Integer.to_string(e, 16)}:#{Integer.to_string(f, 16)}:#{Integer.to_string(g, 16)}:#{Integer.to_string(h, 16)}" + _ -> "unknown" + end + end + + defp rate_limit_message(action, retry_after) do + action_name = case action do + :game_creation -> "game creation" + :connection -> "connections" + _ -> "requests" + end + + """ + Rate limit exceeded for #{action_name}. + Please try again in #{retry_after} seconds. + + This protection is in place to ensure service availability for all users. + """ + end +end diff --git a/copi.owasp.org/mix.lock b/copi.owasp.org/mix.lock index 61ab98edf..0d076d619 100644 --- a/copi.owasp.org/mix.lock +++ b/copi.owasp.org/mix.lock @@ -11,7 +11,7 @@ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"}, "ecto_ulid": {:hex, :ecto_ulid, "0.3.0", "fd6426ff30da547d6f5c31e43170ad307cbda2e680c7793c891e9ef86bd68dbe", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "82cb3be73635587700f1a4aec08e4ad894e7b3d2f6ed63236b9f3afd3859c74d"}, "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, @@ -33,22 +33,22 @@ "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix": {:hex, :phoenix, "1.8.2", "75aba5b90081d88a54f2fc6a26453d4e76762ab095ff89be5a3e7cb28bff9300", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "19ea65b4064f17b1ab0515595e4d0ea65742ab068259608d5d7b139a73f47611"}, + "phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.5", "261f21b67aea8162239b2d6d3b4c31efde4daa22a20d80b19c2c0f21b34b270e", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20884bf58a90ff5a5663420f5d2c368e9e15ed1ad5e911daf0916ea3c57f77ac"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "swoosh": {:hex, :swoosh, "1.19.8", "0576f2ea96d1bb3a6e02cc9f79cbd7d497babc49a353eef8dce1a1f9f82d7915", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d7503c2daf0f9899afd8eba9923eeddef4b62e70816e1d3b6766e4d6c60e94ad"}, + "swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, diff --git a/copi.owasp.org/test/copi/rate_limiter_test.exs b/copi.owasp.org/test/copi/rate_limiter_test.exs new file mode 100644 index 000000000..f4eaa4437 --- /dev/null +++ b/copi.owasp.org/test/copi/rate_limiter_test.exs @@ -0,0 +1,185 @@ +defmodule Copi.RateLimiterTest do + use ExUnit.Case, async: true + + alias Copi.RateLimiter + + describe "game creation rate limiting" do + test "allows requests under the limit" do + ip = "127.0.0.1" + + # First request should be allowed + assert {:ok, remaining} = RateLimiter.check_rate(ip, :game_creation) + assert remaining >= 0 + + RateLimiter.record_action(ip, :game_creation) + + # Second request should still be allowed + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :game_creation) + end + + test "blocks requests over the limit" do + ip = "192.168.1.100" + config = RateLimiter.get_config() + max_games = config.game_creation.max_requests + + # Make max_requests number of game creations + for _i <- 1..max_games do + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :game_creation) + RateLimiter.record_action(ip, :game_creation) + end + + # Next request should be blocked + assert {:error, :rate_limited, retry_after} = RateLimiter.check_rate(ip, :game_creation) + assert retry_after > 0 + end + + test "different IPs have independent limits" do + ip1 = "10.0.0.1" + ip2 = "10.0.0.2" + config = RateLimiter.get_config() + max_games = config.game_creation.max_requests + + # Exhaust limit for ip1 + for _i <- 1..max_games do + RateLimiter.check_rate(ip1, :game_creation) + RateLimiter.record_action(ip1, :game_creation) + end + + # ip1 should be blocked + assert {:error, :rate_limited, _} = RateLimiter.check_rate(ip1, :game_creation) + + # ip2 should still be allowed + assert {:ok, _remaining} = RateLimiter.check_rate(ip2, :game_creation) + end + end + + describe "connection rate limiting" do + test "allows connections under the limit" do + ip = "172.16.0.1" + + assert {:ok, remaining} = RateLimiter.check_rate(ip, :connection) + assert remaining >= 0 + + RateLimiter.record_action(ip, :connection) + + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :connection) + end + + test "blocks connections over the limit" do + ip = "172.16.0.2" + config = RateLimiter.get_config() + max_connections = config.connection.max_requests + + # Make max_requests number of connections + for _i <- 1..max_connections do + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :connection) + RateLimiter.record_action(ip, :connection) + end + + # Next connection should be blocked + assert {:error, :rate_limited, retry_after} = RateLimiter.check_rate(ip, :connection) + assert retry_after > 0 + end + end + + describe "player creation rate limiting" do + test "allows player creation under the limit" do + ip = "192.168.2.1" + + assert {:ok, remaining} = RateLimiter.check_rate(ip, :player_creation) + assert remaining >= 0 + + RateLimiter.record_action(ip, :player_creation) + + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :player_creation) + end + + test "blocks player creation over the limit" do + ip = "192.168.2.2" + config = RateLimiter.get_config() + max_players = config.player_creation.max_requests + + # Make max_requests number of player creations + for _i <- 1..max_players do + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :player_creation) + RateLimiter.record_action(ip, :player_creation) + end + + # Next request should be blocked + assert {:error, :rate_limited, retry_after} = RateLimiter.check_rate(ip, :player_creation) + assert retry_after > 0 + end + + test "player creation limit is separate from game creation limit" do + ip = "192.168.2.3" + config = RateLimiter.get_config() + max_games = config.game_creation.max_requests + + # Exhaust game creation limit + for _i <- 1..max_games do + RateLimiter.check_rate(ip, :game_creation) + RateLimiter.record_action(ip, :game_creation) + end + + # Game creation should be blocked + assert {:error, :rate_limited, _} = RateLimiter.check_rate(ip, :game_creation) + + # Player creation should still be allowed (separate limit) + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :player_creation) + end + end + + describe "rate limit window expiration" do + test "allows requests after window expires" do + ip = "192.168.100.1" + + # This test would require waiting for the window to expire + # In a real scenario, you might want to use a mock timer or + # make the window configurable for testing + + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :game_creation) + RateLimiter.record_action(ip, :game_creation) + + # Verify request was recorded + assert {:ok, _remaining} = RateLimiter.check_rate(ip, :game_creation) + end + end + + describe "configuration" do + test "returns current configuration" do + config = RateLimiter.get_config() + + assert is_map(config) + assert Map.has_key?(config, :game_creation) + assert Map.has_key?(config, :player_creation) + assert Map.has_key?(config, :connection) + + assert config.game_creation.max_requests > 0 + assert config.game_creation.window_seconds > 0 + + assert config.player_creation.max_requests > 0 + assert config.player_creation.window_seconds > 0 + + assert config.connection.max_requests > 0 + assert config.connection.window_seconds > 0 + end + end + + describe "IP clearing" do + test "clears rate limit data for an IP" do + ip = "10.20.30.40" + + # Record some actions + RateLimiter.record_action(ip, :game_creation) + RateLimiter.record_action(ip, :connection) + + # Clear the IP + RateLimiter.clear_ip(ip) + + # Should be able to make full limit of requests again + config = RateLimiter.get_config() + assert {:ok, remaining} = RateLimiter.check_rate(ip, :game_creation) + assert remaining == config.game_creation.max_requests + end + end +end diff --git a/copi.owasp.org/test/copi_web/plugs/rate_limiter_test.exs b/copi.owasp.org/test/copi_web/plugs/rate_limiter_test.exs new file mode 100644 index 000000000..4d3e6ec2f --- /dev/null +++ b/copi.owasp.org/test/copi_web/plugs/rate_limiter_test.exs @@ -0,0 +1,60 @@ +defmodule CopiWeb.Plugs.RateLimiterTest do + use CopiWeb.ConnCase, async: true + + alias CopiWeb.Plugs.RateLimiter + alias Copi.RateLimiter, as: RateLimiterServer + + setup do + # Clear any existing state + RateLimiterServer.clear_ip("127.0.0.1") + :ok + end + + describe "rate limiter plug" do + test "allows requests under the limit", %{conn: conn} do + conn = RateLimiter.call(conn, action: :game_creation) + + refute conn.halted + assert get_resp_header(conn, "x-ratelimit-remaining") != [] + end + + test "blocks requests over the limit", %{conn: conn} do + config = RateLimiterServer.get_config() + max_requests = config.game_creation.max_requests + + # Exhaust the rate limit + for _i <- 1..max_requests do + RateLimiterServer.check_rate("127.0.0.1", :game_creation) + RateLimiterServer.record_action("127.0.0.1", :game_creation) + end + + # Next request should be blocked + conn = RateLimiter.call(conn, action: :game_creation) + + assert conn.halted + assert conn.status == 429 + assert get_resp_header(conn, "retry-after") != [] + assert get_resp_header(conn, "x-ratelimit-remaining") == ["0"] + end + + test "sets rate limit headers", %{conn: conn} do + conn = RateLimiter.call(conn, action: :game_creation) + + headers = get_resp_header(conn, "x-ratelimit-remaining") + assert length(headers) > 0 + + [remaining] = headers + assert String.to_integer(remaining) >= 0 + end + + test "handles different actions separately", %{conn: conn} do + # Test game creation + conn1 = RateLimiter.call(conn, action: :game_creation) + refute conn1.halted + + # Test connection (should be independent) + conn2 = RateLimiter.call(conn, action: :connection) + refute conn2.halted + end + end +end diff --git a/cornucopia.owasp.org/data/website/pages/about/en/index.md b/cornucopia.owasp.org/data/website/pages/about/en/index.md index 5ac3017e9..6c2bd7272 100644 --- a/cornucopia.owasp.org/data/website/pages/about/en/index.md +++ b/cornucopia.owasp.org/data/website/pages/about/en/index.md @@ -59,6 +59,7 @@ Cornucopia is developed, maintained, updated and promoted by a worldwide team of - Artim Banyte - Simon Bennetts - Thomas Berson +- Rishii Bharadhwaj - Jorun Kristin Bremseth - Tom Brennan - Graham Bryant diff --git a/cornucopia.owasp.org/package.json b/cornucopia.owasp.org/package.json index c31bae96a..2c49fc3ba 100644 --- a/cornucopia.owasp.org/package.json +++ b/cornucopia.owasp.org/package.json @@ -17,12 +17,12 @@ "devDependencies": { "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/adapter-cloudflare": "^5.0.1", - "@sveltejs/kit": "^2.49.0", + "@sveltejs/kit": "^2.49.2", "@sveltejs/vite-plugin-svelte": "^4.0.4", - "@types/node": "^24.10.1", + "@types/node": "^25.0.1", "@vitest/coverage-v8": "^3.1.4", "dotenv": "^17.2.3", - "svelte": "^5.45.3", + "svelte": "^5.45.10", "svelte-check": "^4.3.4", "svelte-sitemap": "^2.7.1", "then-request": "^6.0.2", diff --git a/cornucopia.owasp.org/pnpm-lock.yaml b/cornucopia.owasp.org/pnpm-lock.yaml index ffaadb3d9..dc777b6b8 100644 --- a/cornucopia.owasp.org/pnpm-lock.yaml +++ b/cornucopia.owasp.org/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: dependencies: '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1))) + version: 3.0.10(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1))) '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -28,50 +28,50 @@ importers: version: 8.0.1 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.45.3) + version: 4.0.1(svelte@5.45.10) svelte-markdown: specifier: ^0.4.1 - version: 0.4.1(svelte@5.45.3) + version: 0.4.1(svelte@5.45.10) sveltekit-i18n: specifier: ^2.4.2 - version: 2.4.2(svelte@5.45.3) + version: 2.4.2(svelte@5.45.10) sync-request: specifier: ^6.1.0 version: 6.1.0 vite-plugin-restart: specifier: ^2.0.0 - version: 2.0.0(vite@5.4.21(@types/node@24.10.1)) + version: 2.0.0(vite@5.4.21(@types/node@25.0.1)) vite-plugin-static-copy: specifier: ^3.1.4 - version: 3.1.4(vite@5.4.21(@types/node@24.10.1)) + version: 3.1.4(vite@5.4.21(@types/node@25.0.1)) devDependencies: '@sveltejs/adapter-auto': specifier: ^7.0.0 - version: 7.0.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1))) + version: 7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1))) '@sveltejs/adapter-cloudflare': specifier: ^5.0.1 - version: 5.0.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(wrangler@3.105.0(@cloudflare/workers-types@4.20250121.0)) + version: 5.0.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(wrangler@3.105.0(@cloudflare/workers-types@4.20250121.0)) '@sveltejs/kit': - specifier: ^2.49.0 - version: 2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + specifier: ^2.49.2 + version: 2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.4 - version: 4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + version: 4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) '@types/node': - specifier: ^24.10.1 - version: 24.10.1 + specifier: ^25.0.1 + version: 25.0.1 '@vitest/coverage-v8': specifier: ^3.1.4 - version: 3.2.4(vitest@3.2.4(@types/node@24.10.1)) + version: 3.2.4(vitest@3.2.4(@types/node@25.0.1)) dotenv: specifier: ^17.2.3 version: 17.2.3 svelte: - specifier: ^5.45.3 - version: 5.45.3 + specifier: ^5.45.10 + version: 5.45.10 svelte-check: specifier: ^4.3.4 - version: 4.3.4(picomatch@4.0.3)(svelte@5.45.3)(typescript@5.9.3) + version: 4.3.4(picomatch@4.0.3)(svelte@5.45.10)(typescript@5.9.3) svelte-sitemap: specifier: ^2.7.1 version: 2.7.1 @@ -86,10 +86,10 @@ importers: version: 5.9.3 vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@24.10.1) + version: 5.4.21(@types/node@25.0.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.10.1) + version: 3.2.4(@types/node@25.0.1) packages: @@ -921,11 +921,6 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@sveltejs/acorn-typescript@1.0.7': - resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} - peerDependencies: - acorn: ^8.9.0 - '@sveltejs/acorn-typescript@1.0.8': resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: @@ -947,8 +942,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.49.0': - resolution: {integrity: sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==} + '@sveltejs/kit@2.49.2': + resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1010,8 +1005,8 @@ packages: '@types/node@10.17.60': resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@25.0.1': + resolution: {integrity: sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==} '@types/node@8.10.66': resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} @@ -1200,8 +1195,8 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} core-util-is@1.0.3: @@ -1263,8 +1258,8 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + devalue@5.6.1: + resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} @@ -1906,8 +1901,8 @@ packages: engines: {node: '>= 14.17.0'} hasBin: true - svelte@5.45.3: - resolution: {integrity: sha512-ngKXNhNvwPzF43QqEhDOue7TQTrG09em1sd4HBxVF0Wr2gopAmdEWan+rgbdgK4fhBtSOTJO8bYU4chUG7VXZQ==} + svelte@5.45.10: + resolution: {integrity: sha512-GiWXq6akkEN3zVDMQ1BVlRolmks5JkEdzD/67mvXOz6drRfuddT5JwsGZjMGSnsTRv/PjAXX8fqBcOr2g2qc/Q==} engines: {node: '>=18'} sveltekit-i18n@2.4.2: @@ -2647,39 +2642,35 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': - dependencies: - acorn: 8.15.0 - '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))': + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))': dependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) - '@sveltejs/adapter-cloudflare@5.0.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(wrangler@3.105.0(@cloudflare/workers-types@4.20250121.0))': + '@sveltejs/adapter-cloudflare@5.0.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(wrangler@3.105.0(@cloudflare/workers-types@4.20250121.0))': dependencies: '@cloudflare/workers-types': 4.20250121.0 - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) esbuild: 0.24.2 worktop: 0.8.0-next.18 wrangler: 3.105.0(@cloudflare/workers-types@4.20250121.0) - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))': dependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) - '@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1))': + '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1))': dependencies: '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 - cookie: 1.0.2 - devalue: 5.5.0 + cookie: 1.1.1 + devalue: 5.6.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -2687,34 +2678,34 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.45.3 - vite: 5.4.21(@types/node@24.10.1) + svelte: 5.45.10 + vite: 5.4.21(@types/node@25.0.1) - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) debug: 4.4.0 - svelte: 5.45.3 - vite: 5.4.21(@types/node@24.10.1) + svelte: 5.45.10 + vite: 5.4.21(@types/node@25.0.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1))': + '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)))(svelte@5.45.3)(vite@5.4.21(@types/node@24.10.1)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)))(svelte@5.45.10)(vite@5.4.21(@types/node@25.0.1)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 - svelte: 5.45.3 - vite: 5.4.21(@types/node@24.10.1) - vitefu: 1.0.5(vite@5.4.21(@types/node@24.10.1)) + svelte: 5.45.10 + vite: 5.4.21(@types/node@25.0.1) + vitefu: 1.0.5(vite@5.4.21(@types/node@25.0.1)) transitivePeerDependencies: - supports-color - '@sveltekit-i18n/base@1.3.7(svelte@5.45.3)': + '@sveltekit-i18n/base@1.3.7(svelte@5.45.10)': dependencies: - svelte: 5.45.3 + svelte: 5.45.10 '@sveltekit-i18n/parser-default@1.1.1': {} @@ -2724,7 +2715,7 @@ snapshots: '@types/concat-stream@1.6.1': dependencies: - '@types/node': 24.10.1 + '@types/node': 25.0.1 '@types/cookie@0.6.0': {} @@ -2734,7 +2725,7 @@ snapshots: '@types/form-data@0.0.33': dependencies: - '@types/node': 24.10.1 + '@types/node': 25.0.1 '@types/js-yaml@4.0.9': {} @@ -2742,7 +2733,7 @@ snapshots: '@types/node@10.17.60': {} - '@types/node@24.10.1': + '@types/node@25.0.1': dependencies: undici-types: 7.16.0 @@ -2750,7 +2741,7 @@ snapshots: '@types/qs@6.9.17': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.0.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -2765,7 +2756,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.10.1) + vitest: 3.2.4(@types/node@25.0.1) transitivePeerDependencies: - supports-color @@ -2777,13 +2768,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@24.10.1))': + '@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.0.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@24.10.1) + vite: 5.4.21(@types/node@25.0.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2952,7 +2943,7 @@ snapshots: cookie@0.7.2: {} - cookie@1.0.2: {} + cookie@1.1.1: {} core-util-is@1.0.3: {} @@ -2991,7 +2982,7 @@ snapshots: delayed-stream@1.0.0: {} - devalue@5.5.0: {} + devalue@5.6.1: {} dotenv@17.2.3: {} @@ -3725,19 +3716,19 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.45.3)(typescript@5.9.3): + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.45.10)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.45.3 + svelte: 5.45.10 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-i18n@4.0.1(svelte@5.45.3): + svelte-i18n@4.0.1(svelte@5.45.10): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -3745,14 +3736,14 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.11 sade: 1.8.1 - svelte: 5.45.3 + svelte: 5.45.10 tiny-glob: 0.2.9 - svelte-markdown@0.4.1(svelte@5.45.3): + svelte-markdown@0.4.1(svelte@5.45.10): dependencies: '@types/marked': 5.0.2 marked: 5.1.2 - svelte: 5.45.3 + svelte: 5.45.10 svelte-sitemap@2.7.1: dependencies: @@ -3760,7 +3751,7 @@ snapshots: minimist: 1.2.8 xmlbuilder2: 3.1.1 - svelte@5.45.3: + svelte@5.45.10: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -3770,7 +3761,7 @@ snapshots: aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.5.0 + devalue: 5.6.1 esm-env: 1.2.2 esrap: 2.2.1 is-reference: 3.0.3 @@ -3778,11 +3769,11 @@ snapshots: magic-string: 0.30.21 zimmerframe: 1.1.4 - sveltekit-i18n@2.4.2(svelte@5.45.3): + sveltekit-i18n@2.4.2(svelte@5.45.10): dependencies: - '@sveltekit-i18n/base': 1.3.7(svelte@5.45.3) + '@sveltekit-i18n/base': 1.3.7(svelte@5.45.10) '@sveltekit-i18n/parser-default': 1.1.1 - svelte: 5.45.3 + svelte: 5.45.10 sync-request@6.1.0: dependencies: @@ -3876,13 +3867,13 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@3.2.4(@types/node@24.10.1): + vite-node@3.2.4(@types/node@25.0.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.21(@types/node@24.10.1) + vite: 5.4.21(@types/node@25.0.1) transitivePeerDependencies: - '@types/node' - less @@ -3894,37 +3885,37 @@ snapshots: - supports-color - terser - vite-plugin-restart@2.0.0(vite@5.4.21(@types/node@24.10.1)): + vite-plugin-restart@2.0.0(vite@5.4.21(@types/node@25.0.1)): dependencies: micromatch: 4.0.8 - vite: 5.4.21(@types/node@24.10.1) + vite: 5.4.21(@types/node@25.0.1) - vite-plugin-static-copy@3.1.4(vite@5.4.21(@types/node@24.10.1)): + vite-plugin-static-copy@3.1.4(vite@5.4.21(@types/node@25.0.1)): dependencies: chokidar: 3.6.0 p-map: 7.0.3 picocolors: 1.1.1 tinyglobby: 0.2.15 - vite: 5.4.21(@types/node@24.10.1) + vite: 5.4.21(@types/node@25.0.1) - vite@5.4.21(@types/node@24.10.1): + vite@5.4.21(@types/node@25.0.1): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.52.5 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 25.0.1 fsevents: 2.3.3 - vitefu@1.0.5(vite@5.4.21(@types/node@24.10.1)): + vitefu@1.0.5(vite@5.4.21(@types/node@25.0.1)): optionalDependencies: - vite: 5.4.21(@types/node@24.10.1) + vite: 5.4.21(@types/node@25.0.1) - vitest@3.2.4(@types/node@24.10.1): + vitest@3.2.4(@types/node@25.0.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@24.10.1)) + '@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.0.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3942,11 +3933,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@24.10.1) - vite-node: 3.2.4(@types/node@24.10.1) + vite: 5.4.21(@types/node@25.0.1) + vite-node: 3.2.4(@types/node@25.0.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 25.0.1 transitivePeerDependencies: - less - lightningcss diff --git a/install_cornucopia_deps.txt b/install_cornucopia_deps.txt index 9fddd7b0f..44f3a402f 100644 --- a/install_cornucopia_deps.txt +++ b/install_cornucopia_deps.txt @@ -1,9 +1,9 @@ -black == 25.1.0 --hash=sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171 --hash=sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7 +black == 25.12.0 --hash=sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783 --hash=sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f --hash=sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5 --hash=sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf --hash=sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43 --hash=sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea --hash=sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be --hash=sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f --hash=sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d --hash=sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892 --hash=sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a --hash=sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828 --hash=sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655 --hash=sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a --hash=sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b --hash=sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7 --hash=sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f --hash=sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83 --hash=sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f --hash=sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5 --hash=sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5 --hash=sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da --hash=sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce --hash=sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59 --hash=sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a --hash=sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b --hash=sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8 defusedxml == 0.7.1 --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 flake8 == 7.2.0 --hash=sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343 --hash=sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426 httpretty == 1.1.4 --hash=sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68 mypy == 1.18.2 --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 -pytest == 8.3.5 --hash=sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845 +pytest == 9.0.2 --hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b --hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11 pytest-cov == 6.2.1 --hash=sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2 --hash=sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5 freezegun == 1.5.5 --hash=sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a --hash=sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2 atheris == 2.3.0 --hash=sha256:b91cb296d60915c3efa4f6db48f09c4678b574cddb7ca98035f1cb9d9fb96f64 diff --git a/requirements.txt b/requirements.txt index 1ae8790bb..0e691b241 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ packaging == 25.0 --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f -pipenv == 2025.0.4 --hash=sha256:36fc2a7841ccdb2f58a9f787b296c2e15dea3b5b79b84d4071812f28b7e8d7a2 --hash=sha256:e1fbe4cfd25ab179f123d1fbb1fa1cdc0b3ffcdb1f21c775dcaa12ccc356f2bb +pipenv == 2025.1.1 --hash=sha256:c309b5fba6535aca22fd66d3dfcf2d3ca1dd0cae12de52cabecbdddfbfe48938 --hash=sha256:f5acaa089282e93faa24a395a7ecba63d20ca1f477f41aed8261563b7e8f32fb virtualenv == 20.35.1 --hash=sha256:041dac43b6899858a91838b616599e80000e545dee01a21172a6a46746472cb2 --hash=sha256:1d9d93cd01d35b785476e2fa7af711a98d40d227a078941695bbae394f8737e2 virtualenv-clone == 0.5.7 --hash=sha256:44d5263bceed0bac3e1424d64f798095233b64def1c5689afa43dc3223caf5b0 certifi == 2025.8.3 --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 setuptools == 80.9.0 --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c -platformdirs == 4.4.0 --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf +platformdirs == 4.5.1 --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 filelock == 3.19.1 --hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 --hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d distlib == 0.4.0 --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d diff --git a/source/webapp-mappings-2.2.yaml b/source/webapp-mappings-2.2.yaml index 754e8c937..178ccad93 100644 --- a/source/webapp-mappings-2.2.yaml +++ b/source/webapp-mappings-2.2.yaml @@ -6,7 +6,7 @@ meta: version: "2.2" layouts: ["cards", "leaflet", "guide"] templates: ["bridge_qr", "bridge", "tarot", "tarot_qr"] - languages: ["en", "es", "fr", "nl", "nl", "no-nb", "pt-br", "pt-pt", "it", "ru", "hu"] + languages: ["en", "es", "fr", "nl", "no-nb", "pt-br", "pt-pt", "it", "ru", "hu"] suits: - id: "VE"