From e4501c669723c7c783012aacd92486516d6290d8 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Tue, 6 Jan 2026 13:09:01 -0500 Subject: [PATCH] Add: bug bounty platform project --- PROJECTS/bug-bounty-platform/.env.example | 109 + PROJECTS/bug-bounty-platform/.gitignore | 39 + .../.pre-commit-config.yaml | 36 + PROJECTS/bug-bounty-platform/LICENSE | 21 + PROJECTS/bug-bounty-platform/README.md | 532 ++++ .../bug-bounty-platform/backend/.dockerignore | 85 + .../bug-bounty-platform/backend/.style.yapf | 46 + .../bug-bounty-platform/backend/alembic.ini | 43 + .../backend/alembic/env.py | 124 + .../backend/alembic/script.py.mako | 24 + .../versions/20251224_033104_initial.py | 67 + .../20260106_010122_add_bug_bounty_models.py | 152 + .../backend/app/__main__.py | 42 + .../backend/app/admin/__init__.py | 9 + .../backend/app/admin/dependencies.py | 25 + .../backend/app/admin/py.typed | 0 .../backend/app/admin/repository.py | 314 ++ .../backend/app/admin/routes.py | 309 ++ .../backend/app/admin/schemas.py | 136 + .../backend/app/admin/service.py | 303 ++ .../backend/app/auth/RefreshToken.py | 117 + .../backend/app/auth/__init__.py | 4 + .../backend/app/auth/dependencies.py | 21 + .../backend/app/auth/py.typed | 0 .../backend/app/auth/repository.py | 178 ++ .../backend/app/auth/routes.py | 154 + .../backend/app/auth/schemas.py | 78 + .../backend/app/auth/service.py | 213 ++ .../bug-bounty-platform/backend/app/config.py | 213 ++ .../backend/app/core/Base.py | 84 + .../backend/app/core/__init__.py | 4 + .../backend/app/core/base_repository.py | 106 + .../backend/app/core/base_schema.py | 50 + .../backend/app/core/common_schemas.py | 34 + .../backend/app/core/constants.py | 44 + .../backend/app/core/database.py | 160 + .../backend/app/core/dependencies.py | 146 + .../backend/app/core/enums.py | 139 + .../backend/app/core/error_schemas.py | 27 + .../backend/app/core/exceptions.py | 340 +++ .../backend/app/core/health_routes.py | 79 + .../backend/app/core/logging.py | 79 + .../backend/app/core/py.typed | 0 .../backend/app/core/rate_limit.py | 44 + .../backend/app/core/responses.py | 45 + .../backend/app/core/security.py | 190 ++ .../backend/app/factory.py | 145 + .../backend/app/middleware/__init__.py | 11 + .../backend/app/middleware/correlation.py | 43 + .../backend/app/middleware/py.typed | 0 .../backend/app/program/Asset.py | 64 + .../backend/app/program/Program.py | 114 + .../backend/app/program/RewardTier.py | 60 + .../backend/app/program/__init__.py | 4 + .../backend/app/program/dependencies.py | 21 + .../backend/app/program/repository.py | 232 ++ .../backend/app/program/routes.py | 279 ++ .../backend/app/program/schemas.py | 179 ++ .../backend/app/program/service.py | 375 +++ .../bug-bounty-platform/backend/app/py.typed | 0 .../backend/app/report/Attachment.py | 64 + .../backend/app/report/Comment.py | 54 + .../backend/app/report/Report.py | 185 ++ .../backend/app/report/__init__.py | 4 + .../backend/app/report/dependencies.py | 21 + .../backend/app/report/repository.py | 256 ++ .../backend/app/report/routes.py | 251 ++ .../backend/app/report/schemas.py | 141 + .../backend/app/report/service.py | 381 +++ .../backend/app/user/User.py | 126 + .../backend/app/user/__init__.py | 4 + .../backend/app/user/dependencies.py | 21 + .../backend/app/user/py.typed | 0 .../backend/app/user/repository.py | 111 + .../backend/app/user/routes.py | 78 + .../backend/app/user/schemas.py | 109 + .../backend/app/user/service.py | 221 ++ .../bug-bounty-platform/backend/conftest.py | 291 ++ .../backend/pyproject.toml | 287 ++ .../backend/tests/__init__.py | 4 + .../backend/tests/integration/__init__.py | 4 + .../backend/tests/integration/test_admin.py | 256 ++ .../backend/tests/integration/test_auth.py | 288 ++ .../backend/tests/integration/test_health.py | 40 + .../backend/tests/integration/test_users.py | 240 ++ .../backend/tests/unit/__init__.py | 4 + PROJECTS/bug-bounty-platform/backend/uv.lock | 2068 +++++++++++++ PROJECTS/bug-bounty-platform/compose.yml | 158 + PROJECTS/bug-bounty-platform/dev.compose.yml | 128 + .../frontend/.dockerignore | 15 + .../bug-bounty-platform/frontend/.gitignore | 25 + .../frontend/.stylelintignore | 22 + .../bug-bounty-platform/frontend/biome.json | 94 + .../bug-bounty-platform/frontend/index.html | 48 + .../bug-bounty-platform/frontend/package.json | 50 + .../frontend/pnpm-lock.yaml | 2598 +++++++++++++++++ .../public/assets/android-chrome-192x192.png | Bin 0 -> 25807 bytes .../public/assets/android-chrome-512x512.png | Bin 0 -> 73395 bytes .../public/assets/apple-touch-icon.png | Bin 0 -> 22963 bytes .../frontend/public/assets/favicon-16x16.png | Bin 0 -> 746 bytes .../frontend/public/assets/favicon-32x32.png | Bin 0 -> 2032 bytes .../frontend/public/assets/favicon.ico | Bin 0 -> 15406 bytes .../frontend/public/assets/site.webmanifest | 1 + .../bug-bounty-platform/frontend/src/App.tsx | 36 + .../frontend/src/api/hooks/index.ts | 10 + .../frontend/src/api/hooks/useAdmin.ts | 600 ++++ .../frontend/src/api/hooks/useAuth.ts | 243 ++ .../frontend/src/api/hooks/usePrograms.ts | 337 +++ .../frontend/src/api/hooks/useReports.ts | 323 ++ .../frontend/src/api/hooks/useUsers.ts | 138 + .../frontend/src/api/index.ts | 7 + .../frontend/src/api/types/admin.types.ts | 187 ++ .../frontend/src/api/types/auth.types.ts | 140 + .../frontend/src/api/types/index.ts | 10 + .../frontend/src/api/types/program.types.ts | 183 ++ .../frontend/src/api/types/report.types.ts | 177 ++ .../frontend/src/api/types/user.types.ts | 127 + .../frontend/src/config.ts | 208 ++ .../frontend/src/core/api/api.config.ts | 160 + .../frontend/src/core/api/errors.ts | 114 + .../frontend/src/core/api/index.ts | 8 + .../frontend/src/core/api/query.config.ts | 105 + .../frontend/src/core/app/protected-route.tsx | 47 + .../frontend/src/core/app/routers.tsx | 119 + .../frontend/src/core/app/shell.module.scss | 375 +++ .../frontend/src/core/app/shell.tsx | 227 ++ .../frontend/src/core/app/toast.module.scss | 67 + .../frontend/src/core/lib/auth.form.store.ts | 43 + .../frontend/src/core/lib/auth.store.ts | 105 + .../frontend/src/core/lib/index.ts | 10 + .../src/core/lib/program.form.store.ts | 68 + .../src/core/lib/settings.form.store.ts | 36 + .../frontend/src/core/lib/shell.ui.store.ts | 63 + .../bug-bounty-platform/frontend/src/main.tsx | 15 + .../src/routes/admin/admin.module.scss | 463 +++ .../frontend/src/routes/admin/index.tsx | 414 +++ .../src/routes/admin/programs/index.tsx | 313 ++ .../admin/programs/programs.module.scss | 480 +++ .../src/routes/admin/reports/index.tsx | 321 ++ .../routes/admin/reports/reports.module.scss | 497 ++++ .../frontend/src/routes/admin/stats/index.tsx | 104 + .../src/routes/admin/stats/stats.module.scss | 115 + .../routes/company/inbox/inbox.module.scss | 254 ++ .../src/routes/company/inbox/index.tsx | 115 + .../company/programs/[id]/edit.module.scss | 375 +++ .../src/routes/company/programs/[id]/edit.tsx | 439 +++ .../src/routes/company/programs/index.tsx | 120 + .../routes/company/programs/new.module.scss | 257 ++ .../src/routes/company/programs/new.tsx | 222 ++ .../company/programs/programs.module.scss | 234 ++ .../src/routes/company/reports/[id].tsx | 392 +++ .../routes/company/reports/triage.module.scss | 450 +++ .../routes/dashboard/dashboard.module.scss | 314 ++ .../frontend/src/routes/dashboard/index.tsx | 159 + .../frontend/src/routes/landing/index.tsx | 84 + .../src/routes/landing/landing.module.scss | 157 + .../frontend/src/routes/login/index.tsx | 118 + .../src/routes/login/login.module.scss | 194 ++ .../routes/programs/[slug]/detail.module.scss | 323 ++ .../src/routes/programs/[slug]/index.tsx | 198 ++ .../routes/programs/[slug]/submit.module.scss | 208 ++ .../src/routes/programs/[slug]/submit.tsx | 189 ++ .../frontend/src/routes/programs/index.tsx | 114 + .../src/routes/programs/programs.module.scss | 179 ++ .../frontend/src/routes/register/index.tsx | 168 ++ .../src/routes/register/register.module.scss | 194 ++ .../routes/reports/[id]/detail.module.scss | 337 +++ .../src/routes/reports/[id]/index.tsx | 276 ++ .../frontend/src/routes/reports/index.tsx | 189 ++ .../src/routes/reports/reports.module.scss | 320 ++ .../frontend/src/routes/settings/index.tsx | 282 ++ .../src/routes/settings/settings.module.scss | 246 ++ .../frontend/src/styles.scss | 29 + .../frontend/src/styles/_fonts.scss | 12 + .../frontend/src/styles/_index.scss | 8 + .../frontend/src/styles/_mixins.scss | 120 + .../frontend/src/styles/_reset.scss | 198 ++ .../frontend/src/styles/_tokens.scss | 181 ++ .../frontend/stylelint.config.js | 107 + .../frontend/tsconfig.app.json | 31 + .../frontend/tsconfig.json | 7 + .../frontend/tsconfig.node.json | 24 + .../frontend/vite.config.ts | 71 + .../infra/docker/fastapi.dev | 34 + .../infra/docker/fastapi.prod | 59 + .../infra/docker/frontend-builder.prod | 60 + .../bug-bounty-platform/infra/docker/vite.dev | 25 + .../bug-bounty-platform/infra/nginx/dev.nginx | 71 + .../infra/nginx/nginx.conf | 87 + .../infra/nginx/nginx.prod.conf | 81 + .../infra/nginx/prod.nginx | 94 + PROJECTS/bug-bounty-platform/justfile | 266 ++ .../bug-bounty-platform/learn/ARCHITECTURE.md | 1069 +++++++ .../bug-bounty-platform/learn/DATABASE.md | 810 +++++ .../learn/GETTING-STARTED.md | 921 ++++++ .../bug-bounty-platform/learn/PATTERNS.md | 821 ++++++ .../bug-bounty-platform/learn/SECURITY.md | 964 ++++++ 197 files changed, 35392 insertions(+) create mode 100644 PROJECTS/bug-bounty-platform/.env.example create mode 100644 PROJECTS/bug-bounty-platform/.gitignore create mode 100644 PROJECTS/bug-bounty-platform/.pre-commit-config.yaml create mode 100644 PROJECTS/bug-bounty-platform/LICENSE create mode 100644 PROJECTS/bug-bounty-platform/README.md create mode 100644 PROJECTS/bug-bounty-platform/backend/.dockerignore create mode 100755 PROJECTS/bug-bounty-platform/backend/.style.yapf create mode 100644 PROJECTS/bug-bounty-platform/backend/alembic.ini create mode 100644 PROJECTS/bug-bounty-platform/backend/alembic/env.py create mode 100644 PROJECTS/bug-bounty-platform/backend/alembic/script.py.mako create mode 100644 PROJECTS/bug-bounty-platform/backend/alembic/versions/20251224_033104_initial.py create mode 100644 PROJECTS/bug-bounty-platform/backend/alembic/versions/20260106_010122_add_bug_bounty_models.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/__main__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/dependencies.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/py.typed create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/repository.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/routes.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/admin/service.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/RefreshToken.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/dependencies.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/py.typed create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/repository.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/routes.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/auth/service.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/config.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/Base.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/base_repository.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/base_schema.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/common_schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/constants.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/database.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/dependencies.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/enums.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/error_schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/exceptions.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/health_routes.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/logging.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/py.typed create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/rate_limit.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/responses.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/core/security.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/factory.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/middleware/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/middleware/correlation.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/middleware/py.typed create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/Asset.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/Program.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/RewardTier.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/dependencies.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/repository.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/routes.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/program/service.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/py.typed create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/Attachment.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/Comment.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/Report.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/dependencies.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/repository.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/routes.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/report/service.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/User.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/dependencies.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/py.typed create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/repository.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/routes.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/schemas.py create mode 100644 PROJECTS/bug-bounty-platform/backend/app/user/service.py create mode 100644 PROJECTS/bug-bounty-platform/backend/conftest.py create mode 100644 PROJECTS/bug-bounty-platform/backend/pyproject.toml create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/integration/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/integration/test_admin.py create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/integration/test_auth.py create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/integration/test_health.py create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/integration/test_users.py create mode 100644 PROJECTS/bug-bounty-platform/backend/tests/unit/__init__.py create mode 100644 PROJECTS/bug-bounty-platform/backend/uv.lock create mode 100644 PROJECTS/bug-bounty-platform/compose.yml create mode 100644 PROJECTS/bug-bounty-platform/dev.compose.yml create mode 100644 PROJECTS/bug-bounty-platform/frontend/.dockerignore create mode 100644 PROJECTS/bug-bounty-platform/frontend/.gitignore create mode 100755 PROJECTS/bug-bounty-platform/frontend/.stylelintignore create mode 100644 PROJECTS/bug-bounty-platform/frontend/biome.json create mode 100644 PROJECTS/bug-bounty-platform/frontend/index.html create mode 100644 PROJECTS/bug-bounty-platform/frontend/package.json create mode 100644 PROJECTS/bug-bounty-platform/frontend/pnpm-lock.yaml create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/android-chrome-192x192.png create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/android-chrome-512x512.png create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/apple-touch-icon.png create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/favicon-16x16.png create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/favicon-32x32.png create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/favicon.ico create mode 100644 PROJECTS/bug-bounty-platform/frontend/public/assets/site.webmanifest create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/App.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/hooks/index.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAdmin.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAuth.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/hooks/usePrograms.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useReports.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useUsers.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/index.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/types/admin.types.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/types/auth.types.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/types/index.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/types/program.types.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/types/report.types.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/api/types/user.types.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/config.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/api/api.config.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/api/errors.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/api/index.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/api/query.config.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/app/protected-route.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/app/routers.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/app/toast.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.form.store.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.store.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/lib/index.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/lib/program.form.store.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/lib/settings.form.store.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/core/lib/shell.ui.store.ts create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/main.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/admin.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/programs.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/reports.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/stats.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/inbox.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/new.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/new.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/programs.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/reports/[id].tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/company/reports/triage.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/dashboard/dashboard.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/dashboard/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/landing/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/landing/landing.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/login/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/login/login.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/programs/[slug]/detail.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/programs/[slug]/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/programs/[slug]/submit.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/programs/[slug]/submit.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/programs/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/programs/programs.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/register/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/register/register.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/reports/[id]/detail.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/reports/[id]/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/reports/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/reports/reports.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/settings/index.tsx create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/routes/settings/settings.module.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/styles.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/styles/_fonts.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/styles/_index.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/styles/_mixins.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/styles/_reset.scss create mode 100644 PROJECTS/bug-bounty-platform/frontend/src/styles/_tokens.scss create mode 100755 PROJECTS/bug-bounty-platform/frontend/stylelint.config.js create mode 100644 PROJECTS/bug-bounty-platform/frontend/tsconfig.app.json create mode 100644 PROJECTS/bug-bounty-platform/frontend/tsconfig.json create mode 100644 PROJECTS/bug-bounty-platform/frontend/tsconfig.node.json create mode 100644 PROJECTS/bug-bounty-platform/frontend/vite.config.ts create mode 100644 PROJECTS/bug-bounty-platform/infra/docker/fastapi.dev create mode 100644 PROJECTS/bug-bounty-platform/infra/docker/fastapi.prod create mode 100644 PROJECTS/bug-bounty-platform/infra/docker/frontend-builder.prod create mode 100644 PROJECTS/bug-bounty-platform/infra/docker/vite.dev create mode 100644 PROJECTS/bug-bounty-platform/infra/nginx/dev.nginx create mode 100644 PROJECTS/bug-bounty-platform/infra/nginx/nginx.conf create mode 100644 PROJECTS/bug-bounty-platform/infra/nginx/nginx.prod.conf create mode 100644 PROJECTS/bug-bounty-platform/infra/nginx/prod.nginx create mode 100644 PROJECTS/bug-bounty-platform/justfile create mode 100644 PROJECTS/bug-bounty-platform/learn/ARCHITECTURE.md create mode 100644 PROJECTS/bug-bounty-platform/learn/DATABASE.md create mode 100644 PROJECTS/bug-bounty-platform/learn/GETTING-STARTED.md create mode 100644 PROJECTS/bug-bounty-platform/learn/PATTERNS.md create mode 100644 PROJECTS/bug-bounty-platform/learn/SECURITY.md diff --git a/PROJECTS/bug-bounty-platform/.env.example b/PROJECTS/bug-bounty-platform/.env.example new file mode 100644 index 00000000..45170193 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/.env.example @@ -0,0 +1,109 @@ +# ============================================================================= +# AngelaMos | 2026 +# .env.example +# ============================================================================= +# Copy this file to .env and update values for your environment +# ============================================================================= + +# ============================================================================= +# HOST PORTS (change these to avoid conflicts between projects) +# ============================================================================= +NGINX_HOST_PORT=8420 +BACKEND_HOST_PORT=5420 +FRONTEND_HOST_PORT=3420 +POSTGRES_HOST_PORT=4420 +REDIS_HOST_PORT=6420 + +# ============================================================================= +# Application +# ============================================================================= +APP_NAME=FullStack-Template +ENVIRONMENT=development +DEBUG=true +LOG_LEVEL=INFO +LOG_JSON_FORMAT=false + +# ============================================================================= +# Server (internal container settings) +# ============================================================================= +HOST=0.0.0.0 +PORT=8000 +RELOAD=true + +# ============================================================================= +# PostgreSQL +# ============================================================================= +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=app_db +POSTGRES_HOST=db +POSTGRES_CONTAINER_PORT=5432 + +DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_CONTAINER_PORT}/${POSTGRES_DB} + +DB_POOL_SIZE=20 +DB_MAX_OVERFLOW=10 +DB_POOL_TIMEOUT=30 +DB_POOL_RECYCLE=1800 + +# ============================================================================= +# Redis +# ============================================================================= +REDIS_HOST=redis +REDIS_CONTAINER_PORT=6379 +REDIS_PASSWORD= + +REDIS_URL=redis://${REDIS_HOST}:${REDIS_CONTAINER_PORT} + +# ============================================================================= +# Security / JWT +# ============================================================================= +SECRET_KEY=dev-only-change-this-in-production-minimum-32-characters-long + +JWT_ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=15 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# ============================================================================= +# Admin Bootstrap (optional) +# ============================================================================= +# If set, registering with this email auto-promotes to admin role. +# Leave empty or remove to disable auto-admin (all users register as USER). +ADMIN_EMAIL= + +# ============================================================================= +# CORS (must match HOST PORTS above!) +# ============================================================================= +# Format: http://localhost:,http://localhost: +# Update these if you change the host ports above + +CORS_ORIGINS=http://localhost,http://localhost:8420,http://localhost:3420 + +# ============================================================================= +# Rate Limiting +# ============================================================================= +RATE_LIMIT_DEFAULT=100/minute +RATE_LIMIT_AUTH=20/minute + +# ============================================================================= +# Frontend (Vite) +# ============================================================================= +VITE_API_URL=/api +VITE_API_TARGET=http://localhost:8000 +VITE_APP_TITLE=My App + +# ============================================================================= +# Cloudflare Tunnel (Production only - or use whatever deployment method you want) +# ============================================================================= +# Option 1: Cloudflare Tunnel (zero config, no port forwarding needed) +# - Get token from Cloudflare Zero Trust dashboard +# - Access > Tunnels > Create a tunnel > Name it > Copy token below +# - Configure public hostname: yourdomain.com -> http://nginx:80 +# +# Option 2: Traditional hosting (VM + domain + DNS) +# - Host on a VM, buy a domain, create A/CNAME record in Cloudflare DNS +# - Point to your VM's IP, expose nginx port, configure reverse proxy/SSL +# +# Option 3: Whatever works for you +# - Just make sure your nginx container is accessible and SSL is configured +CLOUDFLARE_TUNNEL_TOKEN= diff --git a/PROJECTS/bug-bounty-platform/.gitignore b/PROJECTS/bug-bounty-platform/.gitignore new file mode 100644 index 00000000..0c265239 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/.gitignore @@ -0,0 +1,39 @@ +# AngelaMos | 2025 +# dev.compose.yml + +venv +*.venv +*.env +*.cache +*.egg + +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.mypy_cache/ +.dmypy.json +dmypy.json + +.DS_Store + diff --git a/PROJECTS/bug-bounty-platform/.pre-commit-config.yaml b/PROJECTS/bug-bounty-platform/.pre-commit-config.yaml new file mode 100644 index 00000000..3f9cb2ca --- /dev/null +++ b/PROJECTS/bug-bounty-platform/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +# .pre-commit-config.yaml + +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.8 + hooks: + - id: ruff + args: ["backend/"] + always_run: true + + - repo: local + hooks: + - id: mypy-check + name: MyPy Type Checking + entry: bash -c 'cd backend && mypy .' + language: system + types: [python] + pass_filenames: false + always_run: true + + - id: pylint-check + name: PyLint Code Quality + entry: bash -c 'cd backend && pylint .' + language: system + types: [python] + pass_filenames: false + always_run: true + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-toml + - id: debug-statements + - id: end-of-file-fixer + - id: check-yaml + args: [--allow-multiple-documents] diff --git a/PROJECTS/bug-bounty-platform/LICENSE b/PROJECTS/bug-bounty-platform/LICENSE new file mode 100644 index 00000000..00afa7bb --- /dev/null +++ b/PROJECTS/bug-bounty-platform/LICENSE @@ -0,0 +1,21 @@ +MIT License + +©AngelaMos | 2026 | CarterPerez-dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROJECTS/bug-bounty-platform/README.md b/PROJECTS/bug-bounty-platform/README.md new file mode 100644 index 00000000..07ca4eea --- /dev/null +++ b/PROJECTS/bug-bounty-platform/README.md @@ -0,0 +1,532 @@ +# Bug Bounty Platform + +A production-ready, enterprise-grade bug bounty platform built with modern web technologies. This platform enables companies to run coordinated vulnerability disclosure programs, allowing security researchers to submit findings and receive rewards. + +**Live Demo:** [bugbounty.carterperez-dev.com](https://bugbounty.carterperez-dev.com) +**API Documentation:** [bugbounty.carterperez-dev.com/api/docs](https://bugbounty.carterperez-dev.com/api/docs) + +--- + +## Overview + +This project demonstrates enterprise-level software architecture with: +- Async-first FastAPI backend with strict type safety +- Modern React frontend with TypeScript +- Production-ready security (JWT with refresh token rotation, Argon2id hashing) +- Advanced design patterns (Dependency Injection, Repository Pattern, Layered Architecture) +- Docker containerization with multi-stage builds +- Database migrations with Alembic +- Comprehensive testing and linting infrastructure + +**Part of:** [Cybersecurity-Projects Repository](https://github.com/CarterPerez-dev/Cybersecurity-Projects) (60+ security-focused projects) + +--- + +## Features + +### Security Researcher Features +- User registration and authentication +- Browse public bug bounty programs +- Submit vulnerability reports with markdown support +- Track report status and receive updates +- Earn reputation and rewards + +### Company Features +- Create and manage bug bounty programs +- Define program scope (assets, reward tiers, SLA) +- Triage incoming vulnerability reports +- Assess severity using CVSS scoring +- Award bounties to researchers +- Communicate via comments and attachments + +### Platform Features +- Role-based access control (Researcher, Company, Admin) +- JWT authentication with refresh token rotation +- Token versioning for instant session invalidation +- Multi-device session management +- Rate limiting on all endpoints +- Comprehensive audit logging +- OpenAPI/Swagger documentation + +--- + +## Tech Stack + +### Backend +- **FastAPI** 0.123.0+ - Modern async Python web framework +- **Python** 3.12+ - Strict typing with mypy +- **PostgreSQL** 18 - Primary database with asyncpg driver +- **Redis** 7 - Caching and session storage +- **SQLAlchemy** 2.0+ - Async ORM +- **Alembic** - Database migrations +- **Pydantic** v2 - Data validation and settings +- **JWT** - Token-based authentication with rotation +- **Argon2id** - Password hashing via pwdlib + +### Frontend +- **React** 19.2+ - UI library +- **TypeScript** 5.9 - Static typing +- **Vite** 7 - Build tool with Rolldown +- **React Router** 7.1 - File-based routing +- **TanStack Query** v5 - Server state management +- **Zustand** - Client state management +- **Axios** - HTTP client +- **SASS** - CSS preprocessing + +### Infrastructure +- **Docker** + **Docker Compose** - Containerization +- **Nginx** - Reverse proxy and static file serving +- **Cloudflare Tunnel** - Zero-config deployment (optional) +- **Gunicorn** + **Uvicorn** - Production ASGI server + +### Development Tools +- **Ruff** - Python linting and formatting +- **Biome** - JavaScript/TypeScript linting +- **MyPy** - Static type checking +- **Pytest** - Testing framework +- **Just** - Task runner (30+ commands) +- **Pre-commit hooks** - Automated quality checks + +--- + +## Getting Started + +You have two options: + +### Option 1: Use the Live API (Easiest) + +The platform is already deployed and running! You can: +- Use the web interface: [bugbounty.carterperez-dev.com](https://bugbounty.carterperez-dev.com) +- Access the API directly: [bugbounty.carterperez-dev.com/api/](https://bugbounty.carterperez-dev.com/api/) +- View API documentation: [bugbounty.carterperez-dev.com/api/docs](https://bugbounty.carterperez-dev.com/api/docs) + +You can build your own client application using the deployed API endpoints. See the OpenAPI documentation for available endpoints and schemas. + +### Option 2: Run It Yourself + +If you want to run the entire platform locally or deploy your own instance: + +#### Prerequisites +- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) +- [Just](https://github.com/casey/just) (task runner) - `cargo install just` or see [installation guide](https://github.com/casey/just#installation) +- Git + +#### Quick Start + +1. **Clone the repository:** + ```bash + git clone https://github.com/CarterPerez-dev/Cybersecurity-Projects.git + cd Cybersecurity-Projects/PROJECTS/bug-bounty-platform + ``` + +2. **Configure environment variables:** + ```bash + cp .env.example .env + ``` + + Edit `.env` and update these critical values: + - `SECRET_KEY` - Generate a secure random string (minimum 32 characters) + - `POSTGRES_PASSWORD` - Set a strong database password + - `ADMIN_EMAIL` - (Optional) First user with this email becomes admin + - `CORS_ORIGINS` - Update if using different ports + +3. **Start the platform (development mode with hot reload):** + ```bash + just dev-up + ``` + + Or in production mode: + ```bash + just up + ``` + +4. **Access the platform:** + - **Frontend:** http://localhost:8420 + - **API:** http://localhost:8420/api + - **API Docs:** http://localhost:8420/api/docs + - **Backend (direct):** http://localhost:5420 + - **Frontend Dev Server:** http://localhost:3420 (dev mode only) + +5. **Apply database migrations:** + ```bash + just migrate head + ``` + +6. **Create your first account:** + - Navigate to http://localhost:8420 + - Click "Register" + - If you set `ADMIN_EMAIL` in `.env`, registering with that email grants admin privileges + +#### Common Commands + +The `justfile` provides 30+ commands for development: + +```bash +just # List all available commands + +# Development +just dev-up # Start in development mode (hot reload) +just dev-down # Stop development containers +just dev-logs backend # View backend logs +just dev-shell backend # Open shell in backend container + +# Production +just up # Start in production mode +just down # Stop production containers +just build # Build all containers +just rebuild # Rebuild without cache + +# Database +just migrate head # Apply all migrations +just migration "message" # Create new migration +just rollback # Rollback last migration +just db-current # Show current migration + +# Linting & Type Checking +just lint # Run ruff + pylint +just ruff-fix # Auto-fix linting issues +just mypy # Type check with mypy +just biome-fix # Fix frontend linting issues +just typecheck # Run all type checks + +# Testing +just test # Run all tests +just test-cov # Run tests with coverage report + +# CI +just ci # Run full CI pipeline (lint + typecheck + test) +``` + +#### Project Structure + +``` +bug-bounty-platform/ +├── backend/ # FastAPI backend (~7,000 lines) +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── core/ # Base classes, database, security, constants and enums, etc. +│ │ │ ├── user/ # User domain +│ │ │ ├── auth/ # Authentication +│ │ │ ├── program/ # Bug bounty programs +│ │ │ ├── report/ # Vulnerability reports +│ │ │ └── admin/ # Admin functionality +│ │ └── config.py # configuration values +│ │ └── factory.py # essentially the 'main.py' file +│ │ └── __main__.py # Where the run command lives +│ ├── alembic/ # Database migrations +│ ├── tests/ # Unit and integration tests +│ └── pyproject.toml # Python dependencies +│ +├── frontend/ # React + TypeScript frontend +│ ├── src/ +│ │ ├── routes/ # Pages +│ │ ├── api/ # API client and hooks +│ │ ├── components/ # Reusable components +│ │ ├── styles/ # SCSS global values +│ │ └── core/ # App configuration, zustand stores (ui state management), api configuration +│ └── package.json +│ +├── infra/ # Docker and Nginx configs +│ ├── nginx/ +│ └── docker/ +│ +├── learn/ # Educational documentation (see below) +├── compose.yml # Production Docker Compose +├── dev.compose.yml # Development Docker Compose +├── justfile # Task runner commands +└── .env.example # Environment variables template +``` + +--- + +## Configuration + +### Environment Variables + +All configuration is done via `.env` file. Key variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `NGINX_HOST_PORT` | External port for Nginx | 8420 | +| `BACKEND_HOST_PORT` | External port for backend API | 5420 | +| `FRONTEND_HOST_PORT` | External port for frontend dev server | 3420 | +| `POSTGRES_HOST_PORT` | External port for PostgreSQL | 4420 | +| `REDIS_HOST_PORT` | External port for Redis | 6420 | +| `SECRET_KEY` | JWT signing key (min 32 chars) | **MUST CHANGE** | +| `POSTGRES_PASSWORD` | Database password | **MUST CHANGE** | +| `ADMIN_EMAIL` | Auto-promote this email to admin | (empty) | +| `ENVIRONMENT` | dev/staging/production | development | +| `ACCESS_TOKEN_EXPIRE_MINUTES` | JWT access token lifetime | 15 | +| `REFRESH_TOKEN_EXPIRE_DAYS` | Refresh token lifetime | 7 | + +See `.env.example` for all available options. + +### Port Configuration + +If the default ports conflict with other services, update these in `.env`: + +```bash +NGINX_HOST_PORT=8420 # Change to any available port +BACKEND_HOST_PORT=5420 # Change to any available port +FRONTEND_HOST_PORT=3420 # Change to any available port +POSTGRES_HOST_PORT=4420 # Change to any available port +REDIS_HOST_PORT=6420 # Change to any available port + +# IMPORTANT: Update CORS_ORIGINS to match NGINX_HOST_PORT +CORS_ORIGINS=http://localhost,http://localhost:8420,http://localhost:3420 +``` + +--- + +## Deployment + +### Option 1: Cloudflare Tunnel (Recommended for beginners) + +No port forwarding or reverse proxy configuration needed! + +1. Create a Cloudflare account and add your domain +2. Go to Zero Trust Dashboard > Access > Tunnels +3. Create a new tunnel, name it, and copy the token +4. Add the token to `.env`: + ```bash + CLOUDFLARE_TUNNEL_TOKEN=your-token-here + ``` +5. Configure public hostname in Cloudflare: + - Public hostname: `yourdomain.com` + - Service: `http://nginx:80` +6. Start the platform: + ```bash + just up + ``` + +Your platform is now live at `https://yourdomain.com`! + +### Option 2: Traditional Hosting (VPS) + +1. Rent a VPS (DigitalOcean, AWS, Linode, etc.) +2. Install Docker and Docker Compose +3. Clone the repository and configure `.env` +4. Point your domain's A record to your VPS IP +5. Configure SSL (Let's Encrypt with Certbot) +6. Start the platform: + ```bash + just up + ``` + +### Option 3: Use the Existing Deployment + +Just use the API at `bugbounty.carterperez-dev.com/api/` - no deployment needed! + +--- + +## Learning Resources + +This project includes comprehensive educational documentation in the `learn/` directory: + +- **[ARCHITECTURE.md](learn/ARCHITECTURE.md)** - Deep dive into system architecture and design decisions +- **[PATTERNS.md](learn/PATTERNS.md)** - Explanation of design patterns used (DI, Repository, etc.) +- **[GETTING-STARTED.md](learn/GETTING-STARTED.md)** - Step-by-step tutorial for building similar applications +- **[DATABASE.md](learn/DATABASE.md)** - Database schema design and migration strategies +- **[SECURITY.md](learn/SECURITY.md)** - Security features and best practices explained + +These documents are designed to help you understand not just *what* the code does, but *why* it's architected this way and *how* you can apply these patterns to your own projects. + +--- + +## API Documentation + +### Interactive Documentation + +When running locally, access interactive API docs at: +- **Swagger UI:** http://localhost:8420/api/docs +- **ReDoc:** http://localhost:8420/api/redoc + +For the live deployment: +- **Swagger UI:** [bugbounty.carterperez-dev.com/api/docs](https://bugbounty.carterperez-dev.com/api/docs) + +### Key Endpoints + +**Authentication:** +- `POST /api/v1/auth/register` - Create new account +- `POST /api/v1/auth/login` - Login (returns access + refresh tokens) +- `POST /api/v1/auth/refresh` - Refresh access token +- `POST /api/v1/auth/logout` - Logout (invalidates refresh token) +- `POST /api/v1/auth/logout-all` - Logout from all devices + +**Users:** +- `GET /api/v1/users/me` - Get current user profile +- `PATCH /api/v1/users/me` - Update profile +- `GET /api/v1/users/{id}` - Get public user profile + +**Programs:** +- `GET /api/v1/programs` - List all programs (paginated) +- `GET /api/v1/programs/{slug}` - Get program details +- `POST /api/v1/programs` - Create program (company only) +- `PATCH /api/v1/programs/{slug}` - Update program (owner only) +- `DELETE /api/v1/programs/{slug}` - Delete program (owner only) + +**Reports:** +- `GET /api/v1/reports` - List your reports +- `GET /api/v1/reports/{id}` - Get report details +- `POST /api/v1/reports` - Submit vulnerability report +- `PATCH /api/v1/reports/{id}` - Update report (various endpoints for status changes) + +**Admin:** +- `GET /api/v1/admin/stats` - Platform statistics +- `GET /api/v1/admin/users` - Manage users +- `GET /api/v1/admin/programs` - Manage programs +- `GET /api/v1/admin/reports` - Manage reports + +All endpoints return JSON and use standard HTTP status codes. + +--- + +## Development + +### Running Tests + +```bash +just test # Run all tests +just test-cov # Run with coverage report +``` + +### Type Checking + +```bash +just mypy # Check backend types +just tsc # Check frontend types +just typecheck # Check all types +``` + +### Linting + +```bash +just lint # Backend: ruff + pylint +just ruff-fix # Auto-fix backend linting issues +just biome-fix # Auto-fix frontend linting issues +just stylelint-fix # Auto-fix SCSS linting issues +``` + +### Database Migrations + +```bash +just migration "Add user reputation field" # Create new migration +just migrate head # Apply all migrations +just rollback # Rollback last migration +just db-history # View migration history +``` + +### Docker Management + +```bash +just dev-shell backend # Open shell in backend container +just dev-shell db # Open psql in database container +just dev-logs nginx # View nginx logs +just ps # List running containers +``` + +--- + +## Architecture Highlights + +### Dependency Injection + +FastAPI's dependency injection system is used extensively: + +```python +from fastapi import Depends +from typing import Annotated + +CurrentUser = Annotated[User, Depends(get_current_user)] + +@router.get("/me") +async def get_me(user: CurrentUser) -> UserSchema: + return user +``` + +### Repository Pattern + +All database operations go through repositories: + +```python +class UserRepository(BaseRepository[User]): + async def find_by_email(self, email: str) -> User | None: + stmt = select(User).where(User.email == email) + result = await self.session.execute(stmt) + return result.scalar_one_or_none() + +# Usage in service layer +async def authenticate_user(email: str, password: str) -> User: + user = await user_repo.find_by_email(email) + if not user or not verify_password(password, user.password_hash): + raise InvalidCredentialsError() + return user +``` + +### Type Safety + +Strict type checking with mypy and TypeScript: + +```python +from typing import Generic, TypeVar + +ModelT = TypeVar("ModelT", bound=Base) + +class BaseRepository(Generic[ModelT]): + def __init__(self, session: AsyncSession, model: type[ModelT]) -> None: + self.session = session + self.model = model +``` + +### Security + +Multiple layers of security: +- JWT tokens with HS256 algorithm +- Token versioning (instant invalidation on password change) +- Refresh token rotation (prevents replay attacks) +- Argon2id password hashing +- Rate limiting (100 req/min default, 20 req/min for auth) +- CORS protection +- Input validation with Pydantic + +See [learn/SECURITY.md](learn/SECURITY.md) for detailed explanations. + +--- + +## Contributing + +This is an educational project demonstrating production-level architecture. Feel free to: +- Fork the repository and build upon it +- Use it as a reference for your own projects +- Submit issues if you find bugs +- Share feedback and suggestions + +--- + +## License + +This project is part of the [Cybersecurity-Projects](https://github.com/CarterPerez-dev/Cybersecurity-Projects) repository. + +© AngelaMos | 2026 + +--- + +## Links + +- **Live Platform:** [bugbounty.carterperez-dev.com](https://bugbounty.carterperez-dev.com) +- **API Docs:** [bugbounty.carterperez-dev.com/api/docs](https://bugbounty.carterperez-dev.com/api/docs) +- **Parent Repository:** [Cybersecurity-Projects](https://github.com/CarterPerez-dev/Cybersecurity-Projects) + +--- + +## Support + +For questions, issues, or discussions: +1. Check the [learn/](learn/) directory for detailed documentation +2. Review the API documentation at `/api/docs` +3. Open an issue in the parent repository +4. Email: [contact information if applicable] + +--- + +**Happy Hacking! 🔒** diff --git a/PROJECTS/bug-bounty-platform/backend/.dockerignore b/PROJECTS/bug-bounty-platform/backend/.dockerignore new file mode 100644 index 00000000..48b05d2b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/.dockerignore @@ -0,0 +1,85 @@ +# =============================== +# © AngelaMos | 2025 +# .dockerignore +# =============================== + +# Virtual environments +.venv/ +venv/ +ENV/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.pyo + +# Type checker / linter caches +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ +.ty_cache/ + +# Coverage +.coverage +.coverage.* +htmlcov/ +coverage.xml +*.cover + +# Build artifacts +dist/ +build/ +*.egg-info/ +*.egg + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Environment files (keep .env.example) +.env +.env.local +.env.*.local + +# Git +.git/ +.gitignore +.gitattributes + +# Docker (don't need these in the image) +Dockerfile* +docker-compose* +.dockerignore + +# Tests (not needed in prod image) +tests/ +conftest.py +pytest.ini +.coveragerc + +# Documentation +docs/ +*.md +*.rst +LICENSE + +# Logs +*.log +logs/ + +# Local databases +*.db +*.sqlite +*.sqlite3 + +# Alembic versions not needed in image (migrations run separately) +# alembic/versions/ + +# Misc +.DS_Store +Thumbs.db +*.bak diff --git a/PROJECTS/bug-bounty-platform/backend/.style.yapf b/PROJECTS/bug-bounty-platform/backend/.style.yapf new file mode 100755 index 00000000..74d83416 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/.style.yapf @@ -0,0 +1,46 @@ +[style] +based_on_style = pep8 +column_limit = 75 +indent_width = 4 +continuation_indent_width = 4 +indent_closing_brackets = false +dedent_closing_brackets = true +indent_blank_lines = false +spaces_before_comment = 2 +spaces_around_power_operator = false +spaces_around_default_or_named_assign = true +space_between_ending_comma_and_closing_bracket = false +space_inside_brackets = false +spaces_around_subscript_colon = true +blank_line_before_nested_class_or_def = false +blank_line_before_class_docstring = false +blank_lines_around_top_level_definition = 2 +blank_lines_between_top_level_imports_and_variables = 2 +blank_line_before_module_docstring = false +split_before_logical_operator = true +split_before_first_argument = true +split_before_named_assigns = true +split_complex_comprehension = true +split_before_expression_after_opening_paren = false +split_before_closing_bracket = true +split_all_comma_separated_values = true +split_all_top_level_comma_separated_values = false +coalesce_brackets = false +each_dict_entry_on_separate_line = true +allow_multiline_lambdas = false +allow_multiline_dictionary_keys = false +split_penalty_import_names = 0 +join_multiple_lines = false +align_closing_bracket_with_visual_indent = true +arithmetic_precedence_indication = false +split_penalty_for_added_line_split = 275 +use_tabs = false +split_before_dot = false +split_arguments_when_comma_terminated = true +i18n_function_call = ['_', 'N_', 'gettext', 'ngettext'] +i18n_comment = ['# Translators:', '# i18n:'] +split_penalty_comprehension = 80 +split_penalty_after_opening_bracket = 280 +split_penalty_before_if_expr = 0 +split_penalty_bitwise_operator = 290 +split_penalty_logical_operator = 0 diff --git a/PROJECTS/bug-bounty-platform/backend/alembic.ini b/PROJECTS/bug-bounty-platform/backend/alembic.ini new file mode 100644 index 00000000..fc4ddbe4 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/alembic.ini @@ -0,0 +1,43 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os +file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s + +sqlalchemy.url = driver://user:pass@localhost/dbname + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/PROJECTS/bug-bounty-platform/backend/alembic/env.py b/PROJECTS/bug-bounty-platform/backend/alembic/env.py new file mode 100644 index 00000000..4128572d --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/alembic/env.py @@ -0,0 +1,124 @@ +""" +ⒸAngelaMos | 2025 +env.py +""" +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from config import settings +from core.Base import Base +from core.enums import SafeEnum +from user.User import User +from auth.RefreshToken import RefreshToken +from program.Program import Program +from program.Asset import Asset +from program.RewardTier import RewardTier +from report.Report import Report +from report.Comment import Comment +from report.Attachment import Attachment + + +config = context.config + + +def render_item(type_, obj, autogen_context): + """ + Custom renderer for alembic autogenerate. + Converts SafeEnum to standard sa.Enum and ensures DateTime uses timezone. + """ + import sqlalchemy as sa + + if isinstance(obj, SafeEnum): + enum_class = obj.enum_class + values = [e.value for e in enum_class] + return f"sa.Enum({', '.join(repr(v) for v in values)}, name='{obj.name}')" + + if isinstance(obj, sa.DateTime): + return "sa.DateTime(timezone=True)" + + return False + + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def get_url() -> str: + """ + Get database URL from settings + """ + return str(settings.DATABASE_URL) + + +def run_migrations_offline() -> None: + """ + Run migrations in 'offline' mode + """ + url = get_url() + context.configure( + url = url, + target_metadata = target_metadata, + literal_binds = True, + dialect_opts = {"paramstyle": "named"}, + compare_type = True, + compare_server_default = True, + render_item = render_item, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + """ + Run migrations with connection + """ + context.configure( + connection = connection, + target_metadata = target_metadata, + compare_type = True, + compare_server_default = True, + render_item = render_item, + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """ + Run migrations in async mode + """ + configuration = config.get_section(config.config_ini_section, {}) + configuration["sqlalchemy.url"] = get_url() + + connectable = async_engine_from_config( + configuration, + prefix = "sqlalchemy.", + poolclass = pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """ + Run migrations in 'online' mode + """ + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/PROJECTS/bug-bounty-platform/backend/alembic/script.py.mako b/PROJECTS/bug-bounty-platform/backend/alembic/script.py.mako new file mode 100644 index 00000000..590f5b3a --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/PROJECTS/bug-bounty-platform/backend/alembic/versions/20251224_033104_initial.py b/PROJECTS/bug-bounty-platform/backend/alembic/versions/20251224_033104_initial.py new file mode 100644 index 00000000..acf88147 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/alembic/versions/20251224_033104_initial.py @@ -0,0 +1,67 @@ +"""initial + +Revision ID: 801b86be184b +Revises: +Create Date: 2025-12-24 03:31:04.712921 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = '801b86be184b' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('email', sa.String(length=320), nullable=False), + sa.Column('hashed_password', sa.String(length=1024), nullable=False), + sa.Column('full_name', sa.String(length=255), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_verified', sa.Boolean(), nullable=False), + sa.Column('role', sa.Enum('unknown', 'user', 'admin', name='userrole'), nullable=False), + sa.Column('token_version', sa.Integer(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')) + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_table('refresh_tokens', + sa.Column('token_hash', sa.String(length=64), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('family_id', sa.Uuid(), nullable=False), + sa.Column('device_id', sa.String(length=255), nullable=True), + sa.Column('device_name', sa.String(length=100), nullable=True), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('is_revoked', sa.Boolean(), nullable=False), + sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_refresh_tokens_user_id_users'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_refresh_tokens')) + ) + op.create_index(op.f('ix_refresh_tokens_expires_at'), 'refresh_tokens', ['expires_at'], unique=False) + op.create_index(op.f('ix_refresh_tokens_family_id'), 'refresh_tokens', ['family_id'], unique=False) + op.create_index(op.f('ix_refresh_tokens_token_hash'), 'refresh_tokens', ['token_hash'], unique=True) + op.create_index(op.f('ix_refresh_tokens_user_id'), 'refresh_tokens', ['user_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_refresh_tokens_user_id'), table_name='refresh_tokens') + op.drop_index(op.f('ix_refresh_tokens_token_hash'), table_name='refresh_tokens') + op.drop_index(op.f('ix_refresh_tokens_family_id'), table_name='refresh_tokens') + op.drop_index(op.f('ix_refresh_tokens_expires_at'), table_name='refresh_tokens') + op.drop_table('refresh_tokens') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/PROJECTS/bug-bounty-platform/backend/alembic/versions/20260106_010122_add_bug_bounty_models.py b/PROJECTS/bug-bounty-platform/backend/alembic/versions/20260106_010122_add_bug_bounty_models.py new file mode 100644 index 00000000..bb755b30 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/alembic/versions/20260106_010122_add_bug_bounty_models.py @@ -0,0 +1,152 @@ +"""add bug bounty models + +Revision ID: de05481ca143 +Revises: 801b86be184b +Create Date: 2026-01-06 01:01:22.847036 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'de05481ca143' +down_revision: Union[str, None] = '801b86be184b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('programs', + sa.Column('company_id', sa.Uuid(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('slug', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('rules', sa.Text(), nullable=True), + sa.Column('response_sla_hours', sa.Integer(), nullable=False), + sa.Column('status', sa.Enum('draft', 'active', 'paused', 'closed', name='programstatus'), nullable=False), + sa.Column('visibility', sa.Enum('public', 'private', 'invite_only', name='programvisibility'), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['company_id'], ['users.id'], name=op.f('fk_programs_company_id_users'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_programs')) + ) + op.create_index(op.f('ix_programs_company_id'), 'programs', ['company_id'], unique=False) + op.create_index(op.f('ix_programs_slug'), 'programs', ['slug'], unique=True) + op.create_index(op.f('ix_programs_status'), 'programs', ['status'], unique=False) + op.create_table('assets', + sa.Column('program_id', sa.Uuid(), nullable=False), + sa.Column('asset_type', sa.Enum('domain', 'api', 'mobile_app', 'source_code', 'hardware', 'other', name='assettype'), nullable=False), + sa.Column('identifier', sa.String(length=500), nullable=False), + sa.Column('in_scope', sa.Boolean(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['program_id'], ['programs.id'], name=op.f('fk_assets_program_id_programs'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_assets')) + ) + op.create_index(op.f('ix_assets_program_id'), 'assets', ['program_id'], unique=False) + op.create_table('reports', + sa.Column('program_id', sa.Uuid(), nullable=False), + sa.Column('researcher_id', sa.Uuid(), nullable=False), + sa.Column('title', sa.String(length=500), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('steps_to_reproduce', sa.Text(), nullable=True), + sa.Column('impact', sa.Text(), nullable=True), + sa.Column('severity_submitted', sa.Enum('critical', 'high', 'medium', 'low', 'informational', name='severity'), nullable=False), + sa.Column('severity_final', sa.Enum('critical', 'high', 'medium', 'low', 'informational', name='severity'), nullable=True), + sa.Column('status', sa.Enum('new', 'triaging', 'needs_more_info', 'accepted', 'duplicate', 'informative', 'not_applicable', 'resolved', 'disclosed', name='reportstatus'), nullable=False), + sa.Column('cvss_score', sa.Numeric(precision=3, scale=1), nullable=True), + sa.Column('cwe_id', sa.String(length=20), nullable=True), + sa.Column('bounty_amount', sa.Integer(), nullable=True), + sa.Column('duplicate_of_id', sa.Uuid(), nullable=True), + sa.Column('triaged_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('disclosed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['duplicate_of_id'], ['reports.id'], name=op.f('fk_reports_duplicate_of_id_reports'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['program_id'], ['programs.id'], name=op.f('fk_reports_program_id_programs'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['researcher_id'], ['users.id'], name=op.f('fk_reports_researcher_id_users'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_reports')) + ) + op.create_index(op.f('ix_reports_program_id'), 'reports', ['program_id'], unique=False) + op.create_index(op.f('ix_reports_researcher_id'), 'reports', ['researcher_id'], unique=False) + op.create_index(op.f('ix_reports_status'), 'reports', ['status'], unique=False) + op.create_table('reward_tiers', + sa.Column('program_id', sa.Uuid(), nullable=False), + sa.Column('severity', sa.Enum('critical', 'high', 'medium', 'low', 'informational', name='severity'), nullable=False), + sa.Column('min_bounty', sa.Integer(), nullable=False), + sa.Column('max_bounty', sa.Integer(), nullable=False), + sa.Column('currency', sa.String(length=3), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['program_id'], ['programs.id'], name=op.f('fk_reward_tiers_program_id_programs'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_reward_tiers')) + ) + op.create_index(op.f('ix_reward_tiers_program_id'), 'reward_tiers', ['program_id'], unique=False) + op.create_table('comments', + sa.Column('report_id', sa.Uuid(), nullable=False), + sa.Column('author_id', sa.Uuid(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('is_internal', sa.Boolean(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['author_id'], ['users.id'], name=op.f('fk_comments_author_id_users'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['report_id'], ['reports.id'], name=op.f('fk_comments_report_id_reports'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_comments')) + ) + op.create_index(op.f('ix_comments_author_id'), 'comments', ['author_id'], unique=False) + op.create_index(op.f('ix_comments_report_id'), 'comments', ['report_id'], unique=False) + op.create_table('attachments', + sa.Column('report_id', sa.Uuid(), nullable=False), + sa.Column('comment_id', sa.Uuid(), nullable=True), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('storage_path', sa.String(length=500), nullable=False), + sa.Column('mime_type', sa.String(length=100), nullable=False), + sa.Column('size_bytes', sa.BigInteger(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], name=op.f('fk_attachments_comment_id_comments'), ondelete='SET NULL'), + sa.ForeignKeyConstraint(['report_id'], ['reports.id'], name=op.f('fk_attachments_report_id_reports'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_attachments')) + ) + op.create_index(op.f('ix_attachments_report_id'), 'attachments', ['report_id'], unique=False) + op.add_column('users', sa.Column('company_name', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('bio', sa.Text(), nullable=True)) + op.add_column('users', sa.Column('website', sa.String(length=500), nullable=True)) + op.add_column('users', sa.Column('reputation_score', sa.Integer(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'reputation_score') + op.drop_column('users', 'website') + op.drop_column('users', 'bio') + op.drop_column('users', 'company_name') + op.drop_index(op.f('ix_attachments_report_id'), table_name='attachments') + op.drop_table('attachments') + op.drop_index(op.f('ix_comments_report_id'), table_name='comments') + op.drop_index(op.f('ix_comments_author_id'), table_name='comments') + op.drop_table('comments') + op.drop_index(op.f('ix_reward_tiers_program_id'), table_name='reward_tiers') + op.drop_table('reward_tiers') + op.drop_index(op.f('ix_reports_status'), table_name='reports') + op.drop_index(op.f('ix_reports_researcher_id'), table_name='reports') + op.drop_index(op.f('ix_reports_program_id'), table_name='reports') + op.drop_table('reports') + op.drop_index(op.f('ix_assets_program_id'), table_name='assets') + op.drop_table('assets') + op.drop_index(op.f('ix_programs_status'), table_name='programs') + op.drop_index(op.f('ix_programs_slug'), table_name='programs') + op.drop_index(op.f('ix_programs_company_id'), table_name='programs') + op.drop_table('programs') + # ### end Alembic commands ### diff --git a/PROJECTS/bug-bounty-platform/backend/app/__main__.py b/PROJECTS/bug-bounty-platform/backend/app/__main__.py new file mode 100644 index 00000000..53488e8c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/__main__.py @@ -0,0 +1,42 @@ +""" +ⒸAngelaMos | 2026 +__main__.py +""" + +import uvicorn + +from config import settings +from factory import create_app + + +app = create_app() + +if __name__ == "__main__": + uvicorn.run( + "app.__main__:app", + host = settings.HOST, + port = settings.PORT, + reload = settings.RELOAD, + ) +""" + +⠀⠀⠀⠀⠀⠴⣦⣤⡀⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⣨⣥⣄⣀⠀⡁⠀⠀⡀⡠⠀⠀⠀⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢠⣾⣿⣷⣮⣷⡦⠥⠈⡶⠮⣤⣀⡠⠀⡀⣐⣀⡈⠁⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⠟⠀⠠⠊⠉⠀⠀⢀⠉⠙⠚⠧⣦⣀⡀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡏⠀⠀⠀⠀⠀⠠⠀⠁⠀⢤⠀⠀⠀⠨⡉⠛⠶⠤⣄⣄⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⡀⠀⠀⢰⠀⠍⡾⠆⠀⠀⣠⡦⠄⡀⠄⠀⠠⠀⠀⠀⠈⠙⠓⠦⢤⣀⡀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣶⣦⢠⡈⠀⠀⠀⠀⠀⠋⠛⠉⡂⠈⠙⠀⣰⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠺⠦⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠻⢿⣿⣿⣿⣿⣿⣾⣿⣿⣦⢤⡀⢀⣂⣨⠀⢅⢱⡔⠒⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠲⠴⣠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣎⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣾⣽⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⠳⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠏⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⠹⣦⣴⠖⠲⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠘⢿⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠈⠀⠀⠀⠒⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢩⠢⣙⠿⣿⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣆⠈⠛⢶⣌⡉⣻⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣷⣄⣤⣙⣿⣿⣿⣷⣄⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠟⠛⠟⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⡿⣿⣿⣿⣿⣿⣿⡿⠋⠉⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠙⠁⠘⢮⣛⡽⠛⠿⡿⠥⠀ + +""" diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/admin/__init__.py new file mode 100644 index 00000000..fe240105 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/admin/__init__.py @@ -0,0 +1,9 @@ +""" +ⒸAngelaMos | 2026 +admin module +""" + +from .routes import router + + +__all__ = ["router"] diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/dependencies.py b/PROJECTS/bug-bounty-platform/backend/app/admin/dependencies.py new file mode 100644 index 00000000..67f26139 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/admin/dependencies.py @@ -0,0 +1,25 @@ +""" +ⒸAngelaMos | 2026 +dependencies.py +""" + +from typing import Annotated + +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from core.database import get_db_session +from .service import AdminService + + +async def get_admin_service( + session: Annotated[AsyncSession, + Depends(get_db_session)], +) -> AdminService: + """ + Dependency for AdminService + """ + return AdminService(session) + + +AdminServiceDep = Annotated[AdminService, Depends(get_admin_service)] diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/py.typed b/PROJECTS/bug-bounty-platform/backend/app/admin/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/repository.py b/PROJECTS/bug-bounty-platform/backend/app/admin/repository.py new file mode 100644 index 00000000..e7333ce1 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/admin/repository.py @@ -0,0 +1,314 @@ +""" +ⒸAngelaMos | 2026 +repository.py +""" + +from typing import Any +from collections.abc import Sequence +from datetime import UTC, datetime, timedelta +from uuid import UUID + +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from config import ProgramStatus, ReportStatus, UserRole +from program.Program import Program +from report.Report import Report +from user.User import User + + +class AdminRepository: + """ + Repository for admin-specific database operations + """ + @classmethod + async def get_all_programs( + cls, + session: AsyncSession, + skip: int = 0, + limit: int = 20, + status_filter: ProgramStatus | None = None, + ) -> Sequence[tuple[Program, + User, + int]]: + """ + Get all programs with company info and report count + """ + report_count_subq = ( + select( + Report.program_id, + func.count(Report.id).label("report_count") + ).group_by(Report.program_id).subquery() + ) + + query = ( + select( + Program, + User, + func.coalesce(report_count_subq.c.report_count, + 0).label("report_count") + ).join(User, + Program.company_id == User.id).outerjoin( + report_count_subq, + Program.id == report_count_subq.c.program_id + ) + ) + + if status_filter: + query = query.where(Program.status == status_filter) + + result = await session.execute( + query.order_by(Program.created_at.desc() + ).offset(skip).limit(limit) + ) + return [tuple(row) for row in result.all()] + + @classmethod + async def count_all_programs( + cls, + session: AsyncSession, + status_filter: ProgramStatus | None = None, + ) -> int: + """ + Count all programs + """ + query = select(func.count()).select_from(Program) + + if status_filter: + query = query.where(Program.status == status_filter) + + result = await session.execute(query) + return result.scalar_one() + + @classmethod + async def get_program_by_id( + cls, + session: AsyncSession, + program_id: UUID, + ) -> Program | None: + """ + Get program by ID for admin operations + """ + return await session.get(Program, program_id) + + @classmethod + async def get_all_reports( + cls, + session: AsyncSession, + skip: int = 0, + limit: int = 20, + status_filter: ReportStatus | None = None, + severity_filter: str | None = None, + ) -> Sequence[tuple[Report, + Program, + User]]: + """ + Get all reports with program and researcher info + """ + query = ( + select(Report, + Program, + User).join(Program, + Report.program_id == Program.id).join( + User, + Report.researcher_id == User.id + ) + ) + + if status_filter: + query = query.where(Report.status == status_filter) + + if severity_filter: + query = query.where( + Report.severity_submitted == severity_filter + ) + + result = await session.execute( + query.order_by(Report.created_at.desc() + ).offset(skip).limit(limit) + ) + return [tuple(row) for row in result.all()] + + @classmethod + async def count_all_reports( + cls, + session: AsyncSession, + status_filter: ReportStatus | None = None, + severity_filter: str | None = None, + ) -> int: + """ + Count all reports + """ + query = select(func.count()).select_from(Report) + + if status_filter: + query = query.where(Report.status == status_filter) + + if severity_filter: + query = query.where( + Report.severity_submitted == severity_filter + ) + + result = await session.execute(query) + return result.scalar_one() + + @classmethod + async def get_report_by_id( + cls, + session: AsyncSession, + report_id: UUID, + ) -> Report | None: + """ + Get report by ID for admin operations + """ + return await session.get(Report, report_id) + + @classmethod + async def get_all_users_with_stats( + cls, + session: AsyncSession, + skip: int = 0, + limit: int = 20, + role_filter: UserRole | None = None, + ) -> Sequence[tuple[User, + int, + int]]: + """ + Get all users with program and report counts + """ + program_count_subq = ( + select( + Program.company_id, + func.count(Program.id).label("program_count") + ).group_by(Program.company_id).subquery() + ) + + report_count_subq = ( + select( + Report.researcher_id, + func.count(Report.id).label("report_count") + ).group_by(Report.researcher_id).subquery() + ) + + query = ( + select( + User, + func.coalesce(program_count_subq.c.program_count, + 0).label("program_count"), + func.coalesce(report_count_subq.c.report_count, + 0).label("report_count") + ).outerjoin( + program_count_subq, + User.id == program_count_subq.c.company_id + ).outerjoin( + report_count_subq, + User.id == report_count_subq.c.researcher_id + ) + ) + + if role_filter: + query = query.where(User.role == role_filter) + + result = await session.execute( + query.order_by(User.created_at.desc() + ).offset(skip).limit(limit) + ) + return [tuple(row) for row in result.all()] + + @classmethod + async def count_all_users( + cls, + session: AsyncSession, + role_filter: UserRole | None = None, + ) -> int: + """ + Count all users + """ + query = select(func.count()).select_from(User) + + if role_filter: + query = query.where(User.role == role_filter) + + result = await session.execute(query) + return result.scalar_one() + + @classmethod + async def get_platform_stats( + cls, + session: AsyncSession, + ) -> dict[str, + Any]: + """ + Get platform-wide statistics + """ + now = datetime.now(UTC) + month_ago = now - timedelta(days = 30) + + total_users = await session.execute( + select(func.count()).select_from(User) + ) + + users_by_role = await session.execute( + select(User.role, + func.count()).group_by(User.role) + ) + role_counts = { + str(role): count + for role, count in users_by_role.all() + } + + total_programs = await session.execute( + select(func.count()).select_from(Program) + ) + + active_programs = await session.execute( + select(func.count()).select_from(Program).where( + Program.status == ProgramStatus.ACTIVE + ) + ) + + total_reports = await session.execute( + select(func.count()).select_from(Report) + ) + + reports_by_status = await session.execute( + select(Report.status, + func.count()).group_by(Report.status) + ) + status_counts = { + str(status): count + for status, count in reports_by_status.all() + } + + total_bounties = await session.execute( + select(func.coalesce(func.sum(Report.bounty_amount), + 0)).where( + Report.bounty_amount.isnot(None) + ) + ) + + reports_this_month = await session.execute( + select(func.count()).select_from(Report).where( + Report.created_at >= month_ago + ) + ) + + new_users_this_month = await session.execute( + select(func.count() + ).select_from(User).where(User.created_at >= month_ago) + ) + + return { + "total_users": total_users.scalar_one(), + "total_researchers": role_counts.get("user", + 0), + "total_companies": role_counts.get("company", + 0), + "total_programs": total_programs.scalar_one(), + "active_programs": active_programs.scalar_one(), + "total_reports": total_reports.scalar_one(), + "reports_by_status": status_counts, + "total_bounties_paid": total_bounties.scalar_one(), + "reports_this_month": reports_this_month.scalar_one(), + "new_users_this_month": new_users_this_month.scalar_one(), + } diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/routes.py b/PROJECTS/bug-bounty-platform/backend/app/admin/routes.py new file mode 100644 index 00000000..fe28d252 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/admin/routes.py @@ -0,0 +1,309 @@ +""" +ⒸAngelaMos | 2026 +routes.py +""" + +from uuid import UUID +from typing import Annotated + +from fastapi import ( + APIRouter, + Depends, + Query, + status, +) + +from config import ( + settings, + ProgramStatus, + ReportStatus, + UserRole, +) +from core.dependencies import RequireRole +from core.responses import ( + AUTH_401, + CONFLICT_409, + FORBIDDEN_403, + NOT_FOUND_404, +) +from user.schemas import ( + AdminUserCreate, + UserResponse, + UserUpdateAdmin, +) +from user.User import User +from user.dependencies import UserServiceDep +from .dependencies import AdminServiceDep +from .schemas import ( + AdminProgramListResponse, + AdminProgramResponse, + AdminProgramUpdate, + AdminReportListResponse, + AdminReportResponse, + AdminReportUpdate, + AdminUserListResponse, + PlatformStatsResponse, +) + + +router = APIRouter(prefix = "/admin", tags = ["admin"]) + +AdminOnly = Annotated[User, Depends(RequireRole(UserRole.ADMIN))] + + +@router.get( + "/stats", + response_model = PlatformStatsResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403 + }, +) +async def get_platform_stats( + admin_service: AdminServiceDep, + _: AdminOnly, +) -> PlatformStatsResponse: + """ + Get platform-wide statistics (admin only) + """ + return await admin_service.get_platform_stats() + + +@router.get( + "/programs", + response_model = AdminProgramListResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403 + }, +) +async def list_programs( + admin_service: AdminServiceDep, + _: AdminOnly, + page: int = Query(default = 1, + ge = 1), + size: int = Query( + default = settings.PAGINATION_DEFAULT_SIZE, + ge = 1, + le = settings.PAGINATION_MAX_SIZE + ), + status_filter: ProgramStatus + | None = Query(default = None, + alias = "status"), +) -> AdminProgramListResponse: + """ + List all programs (admin only) + """ + return await admin_service.list_programs(page, size, status_filter) + + +@router.patch( + "/programs/{program_id}", + response_model = AdminProgramResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def update_program( + admin_service: AdminServiceDep, + _: AdminOnly, + program_id: UUID, + data: AdminProgramUpdate, +) -> AdminProgramResponse: + """ + Update a program (admin only) + """ + return await admin_service.update_program(program_id, data) + + +@router.delete( + "/programs/{program_id}", + status_code = status.HTTP_204_NO_CONTENT, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def delete_program( + admin_service: AdminServiceDep, + _: AdminOnly, + program_id: UUID, +) -> None: + """ + Delete a program (admin only, hard delete) + """ + await admin_service.delete_program(program_id) + + +@router.get( + "/reports", + response_model = AdminReportListResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403 + }, +) +async def list_reports( + admin_service: AdminServiceDep, + _: AdminOnly, + page: int = Query(default = 1, + ge = 1), + size: int = Query( + default = settings.PAGINATION_DEFAULT_SIZE, + ge = 1, + le = settings.PAGINATION_MAX_SIZE + ), + status_filter: ReportStatus + | None = Query(default = None, + alias = "status"), + severity_filter: str + | None = Query(default = None, + alias = "severity"), +) -> AdminReportListResponse: + """ + List all reports (admin only) + """ + return await admin_service.list_reports( + page, + size, + status_filter, + severity_filter + ) + + +@router.patch( + "/reports/{report_id}", + response_model = AdminReportResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def update_report( + admin_service: AdminServiceDep, + _: AdminOnly, + report_id: UUID, + data: AdminReportUpdate, +) -> AdminReportResponse: + """ + Update a report (admin only, override) + """ + return await admin_service.update_report(report_id, data) + + +@router.get( + "/users", + response_model = AdminUserListResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403 + }, +) +async def list_users( + admin_service: AdminServiceDep, + _: AdminOnly, + page: int = Query(default = 1, + ge = 1), + size: int = Query( + default = settings.PAGINATION_DEFAULT_SIZE, + ge = 1, + le = settings.PAGINATION_MAX_SIZE + ), + role_filter: UserRole | None = Query(default = None, + alias = "role"), +) -> AdminUserListResponse: + """ + List all users with stats (admin only) + """ + return await admin_service.list_users_with_stats( + page, + size, + role_filter + ) + + +@router.post( + "/users", + response_model = UserResponse, + status_code = status.HTTP_201_CREATED, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **CONFLICT_409 + }, +) +async def create_user( + user_service: UserServiceDep, + _: AdminOnly, + user_data: AdminUserCreate, +) -> UserResponse: + """ + Create a new user (admin only, bypasses registration) + """ + return await user_service.admin_create_user(user_data) + + +@router.get( + "/users/{user_id}", + response_model = UserResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def get_user( + user_service: UserServiceDep, + _: AdminOnly, + user_id: UUID, +) -> UserResponse: + """ + Get user by ID (admin only) + """ + return await user_service.get_user_by_id(user_id) + + +@router.patch( + "/users/{user_id}", + response_model = UserResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404, + **CONFLICT_409 + }, +) +async def update_user( + user_service: UserServiceDep, + _: AdminOnly, + user_id: UUID, + user_data: UserUpdateAdmin, +) -> UserResponse: + """ + Update user (admin only) + """ + return await user_service.admin_update_user(user_id, user_data) + + +@router.delete( + "/users/{user_id}", + status_code = status.HTTP_204_NO_CONTENT, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def delete_user( + user_service: UserServiceDep, + _: AdminOnly, + user_id: UUID, +) -> None: + """ + Delete user (admin only, hard delete) + """ + await user_service.admin_delete_user(user_id) diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/schemas.py b/PROJECTS/bug-bounty-platform/backend/app/admin/schemas.py new file mode 100644 index 00000000..14ee2481 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/admin/schemas.py @@ -0,0 +1,136 @@ +""" +ⒸAngelaMos | 2026 +schemas.py +""" + +from datetime import datetime +from decimal import Decimal +from uuid import UUID + +from pydantic import Field + +from config import ( + ProgramStatus, + ProgramVisibility, + ReportStatus, + Severity, + UserRole, +) +from core.base_schema import BaseSchema, BaseResponseSchema + + +class AdminProgramUpdate(BaseSchema): + """ + Schema for admin updating a program + """ + name: str | None = None + status: ProgramStatus | None = None + visibility: ProgramVisibility | None = None + is_featured: bool | None = None + + +class AdminProgramResponse(BaseResponseSchema): + """ + Schema for admin program view with company info + """ + company_id: UUID + company_email: str + company_name: str | None + name: str + slug: str + description: str | None + status: ProgramStatus + visibility: ProgramVisibility + response_sla_hours: int + report_count: int + + +class AdminProgramListResponse(BaseSchema): + """ + Schema for paginated admin program list + """ + items: list[AdminProgramResponse] + total: int + page: int + size: int + + +class AdminReportUpdate(BaseSchema): + """ + Schema for admin overriding a report + """ + status: ReportStatus | None = None + severity_final: Severity | None = None + cvss_score: Decimal | None = Field(default = None, ge = 0, le = 10) + bounty_amount: int | None = Field(default = None, ge = 0) + admin_notes: str | None = None + + +class AdminReportResponse(BaseResponseSchema): + """ + Schema for admin report view with program/researcher info + """ + program_id: UUID + program_name: str + program_slug: str + researcher_id: UUID + researcher_email: str + researcher_name: str | None + title: str + severity_submitted: Severity + severity_final: Severity | None + status: ReportStatus + bounty_amount: int | None + triaged_at: datetime | None + resolved_at: datetime | None + + +class AdminReportListResponse(BaseSchema): + """ + Schema for paginated admin report list + """ + items: list[AdminReportResponse] + total: int + page: int + size: int + + +class PlatformStatsResponse(BaseSchema): + """ + Schema for platform-wide statistics + """ + total_users: int + total_researchers: int + total_companies: int + total_programs: int + active_programs: int + total_reports: int + reports_by_status: dict[str, int] + total_bounties_paid: int + reports_this_month: int + new_users_this_month: int + + +class AdminUserResponse(BaseResponseSchema): + """ + Schema for admin user view with stats + """ + email: str + full_name: str | None + company_name: str | None + is_active: bool + is_verified: bool + role: UserRole + reputation_score: int + program_count: int + report_count: int + + +class AdminUserListResponse(BaseSchema): + """ + Schema for paginated admin user list with stats + """ + items: list[AdminUserResponse] + total: int + page: int + size: int diff --git a/PROJECTS/bug-bounty-platform/backend/app/admin/service.py b/PROJECTS/bug-bounty-platform/backend/app/admin/service.py new file mode 100644 index 00000000..11671228 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/admin/service.py @@ -0,0 +1,303 @@ +""" +ⒸAngelaMos | 2026 +service.py +""" + +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession + +from config import ProgramStatus, ReportStatus, UserRole +from core.exceptions import ResourceNotFound +from .repository import AdminRepository +from .schemas import ( + AdminProgramListResponse, + AdminProgramResponse, + AdminProgramUpdate, + AdminReportListResponse, + AdminReportResponse, + AdminReportUpdate, + AdminUserListResponse, + AdminUserResponse, + PlatformStatsResponse, +) + + +class AdminService: + """ + Service for admin operations + """ + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def list_programs( + self, + page: int, + size: int, + status_filter: ProgramStatus | None = None, + ) -> AdminProgramListResponse: + """ + List all programs with company info + """ + skip = (page - 1) * size + + results = await AdminRepository.get_all_programs( + self.session, + skip = skip, + limit = size, + status_filter = status_filter, + ) + + total = await AdminRepository.count_all_programs( + self.session, + status_filter = status_filter, + ) + + items = [ + AdminProgramResponse( + id = program.id, + created_at = program.created_at, + updated_at = program.updated_at, + company_id = program.company_id, + company_email = company.email, + company_name = company.company_name or company.full_name, + name = program.name, + slug = program.slug, + description = program.description, + status = program.status, + visibility = program.visibility, + response_sla_hours = program.response_sla_hours, + report_count = report_count, + ) for program, company, report_count in results + ] + + return AdminProgramListResponse( + items = items, + total = total, + page = page, + size = size, + ) + + async def update_program( + self, + program_id: UUID, + data: AdminProgramUpdate, + ) -> AdminProgramResponse: + """ + Update a program (admin override) + """ + program = await AdminRepository.get_program_by_id( + self.session, + program_id + ) + if not program: + raise ResourceNotFound("Program", str(program_id)) + + update_data = data.model_dump(exclude_unset = True) + for key, value in update_data.items(): + setattr(program, key, value) + + await self.session.flush() + await self.session.refresh(program) + + results = await AdminRepository.get_all_programs( + self.session, + skip = 0, + limit = 1, + status_filter = None + ) + for p, company, report_count in results: + if p.id == program_id: + return AdminProgramResponse( + id = program.id, + created_at = program.created_at, + updated_at = program.updated_at, + company_id = program.company_id, + company_email = company.email, + company_name = company.company_name + or company.full_name, + name = program.name, + slug = program.slug, + description = program.description, + status = program.status, + visibility = program.visibility, + response_sla_hours = program.response_sla_hours, + report_count = report_count, + ) + + raise ResourceNotFound("Program", str(program_id)) + + async def delete_program(self, program_id: UUID) -> None: + """ + Delete a program (hard delete) + """ + program = await AdminRepository.get_program_by_id( + self.session, + program_id + ) + if not program: + raise ResourceNotFound("Program", str(program_id)) + + await self.session.delete(program) + await self.session.flush() + + async def list_reports( + self, + page: int, + size: int, + status_filter: ReportStatus | None = None, + severity_filter: str | None = None, + ) -> AdminReportListResponse: + """ + List all reports with program/researcher info + """ + skip = (page - 1) * size + + results = await AdminRepository.get_all_reports( + self.session, + skip = skip, + limit = size, + status_filter = status_filter, + severity_filter = severity_filter, + ) + + total = await AdminRepository.count_all_reports( + self.session, + status_filter = status_filter, + severity_filter = severity_filter, + ) + + items = [ + AdminReportResponse( + id = report.id, + created_at = report.created_at, + updated_at = report.updated_at, + program_id = report.program_id, + program_name = program.name, + program_slug = program.slug, + researcher_id = report.researcher_id, + researcher_email = researcher.email, + researcher_name = researcher.full_name, + title = report.title, + severity_submitted = report.severity_submitted, + severity_final = report.severity_final, + status = report.status, + bounty_amount = report.bounty_amount, + triaged_at = report.triaged_at, + resolved_at = report.resolved_at, + ) for report, program, researcher in results + ] + + return AdminReportListResponse( + items = items, + total = total, + page = page, + size = size, + ) + + async def update_report( + self, + report_id: UUID, + data: AdminReportUpdate, + ) -> AdminReportResponse: + """ + Update a report (admin override) + """ + report = await AdminRepository.get_report_by_id( + self.session, + report_id + ) + if not report: + raise ResourceNotFound("Report", str(report_id)) + + update_data = data.model_dump( + exclude_unset = True, + exclude = {"admin_notes"} + ) + for key, value in update_data.items(): + setattr(report, key, value) + + await self.session.flush() + await self.session.refresh(report) + + results = await AdminRepository.get_all_reports( + self.session, + skip = 0, + limit = 1000 + ) + for r, program, researcher in results: + if r.id == report_id: + return AdminReportResponse( + id = report.id, + created_at = report.created_at, + updated_at = report.updated_at, + program_id = report.program_id, + program_name = program.name, + program_slug = program.slug, + researcher_id = report.researcher_id, + researcher_email = researcher.email, + researcher_name = researcher.full_name, + title = report.title, + severity_submitted = report.severity_submitted, + severity_final = report.severity_final, + status = report.status, + bounty_amount = report.bounty_amount, + triaged_at = report.triaged_at, + resolved_at = report.resolved_at, + ) + + raise ResourceNotFound("Report", str(report_id)) + + async def list_users_with_stats( + self, + page: int, + size: int, + role_filter: UserRole | None = None, + ) -> AdminUserListResponse: + """ + List all users with program/report counts + """ + skip = (page - 1) * size + + results = await AdminRepository.get_all_users_with_stats( + self.session, + skip = skip, + limit = size, + role_filter = role_filter, + ) + + total = await AdminRepository.count_all_users( + self.session, + role_filter = role_filter, + ) + + items = [ + AdminUserResponse( + id = user.id, + created_at = user.created_at, + updated_at = user.updated_at, + email = user.email, + full_name = user.full_name, + company_name = user.company_name, + is_active = user.is_active, + is_verified = user.is_verified, + role = user.role, + reputation_score = user.reputation_score, + program_count = program_count, + report_count = report_count, + ) for user, program_count, report_count in results + ] + + return AdminUserListResponse( + items = items, + total = total, + page = page, + size = size, + ) + + async def get_platform_stats(self) -> PlatformStatsResponse: + """ + Get platform-wide statistics + """ + stats = await AdminRepository.get_platform_stats(self.session) + return PlatformStatsResponse(**stats) diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/RefreshToken.py b/PROJECTS/bug-bounty-platform/backend/app/auth/RefreshToken.py new file mode 100644 index 00000000..f827bb4f --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/RefreshToken.py @@ -0,0 +1,117 @@ +""" +ⒸAngelaMos | 2025 +RefreshToken.py +""" + +from __future__ import annotations + +from datetime import UTC, datetime +from typing import TYPE_CHECKING +from uuid import UUID + +import uuid6 +from sqlalchemy import ( + DateTime, + ForeignKey, + String, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + DEVICE_ID_MAX_LENGTH, + DEVICE_NAME_MAX_LENGTH, + IP_ADDRESS_MAX_LENGTH, + TOKEN_HASH_LENGTH, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from user.User import User + + +class RefreshToken(Base, UUIDMixin, TimestampMixin): + """ + Refresh token for JWT authentication + + Tokens are stored as SHA 256 hashes, never raw + Family ID enables detection of token reuse attacks + """ + __tablename__ = "refresh_tokens" + + token_hash: Mapped[str] = mapped_column( + String(TOKEN_HASH_LENGTH), + unique = True, + index = True, + ) + + user_id: Mapped[UUID] = mapped_column( + ForeignKey("users.id", + ondelete = "CASCADE"), + index = True, + ) + + family_id: Mapped[UUID] = mapped_column( + default = uuid6.uuid7, + index = True, + ) + + device_id: Mapped[str | None] = mapped_column( + String(DEVICE_ID_MAX_LENGTH), + default = None, + ) + device_name: Mapped[str | None] = mapped_column( + String(DEVICE_NAME_MAX_LENGTH), + default = None, + ) + ip_address: Mapped[str | None] = mapped_column( + String(IP_ADDRESS_MAX_LENGTH), + default = None, + ) + + expires_at: Mapped[datetime] = mapped_column( + DateTime(timezone = True), + index = True, + ) + + is_revoked: Mapped[bool] = mapped_column(default = False) + revoked_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone = True), + default = None, + ) + + user: Mapped[User] = relationship(back_populates = "refresh_tokens") + + def revoke(self) -> None: + """ + Revoke this token + """ + self.is_revoked = True + self.revoked_at = datetime.now(UTC) + + @property + def is_expired(self) -> bool: + """ + Check if token has expired + + Handles both timezone aware and naive datetimes for SQLite compatibility + """ + now = datetime.now(UTC) + expires = self.expires_at + if expires.tzinfo is None: + expires = expires.replace(tzinfo = UTC) + return now > expires + + @property + def is_valid(self) -> bool: + """ + Check if token is usable + """ + return not self.is_revoked and not self.is_expired diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/auth/__init__.py new file mode 100644 index 00000000..e2ef2457 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/__init__.py @@ -0,0 +1,4 @@ +""" +ⒸAngelaMos | 2025 +Auth Domain +""" diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/dependencies.py b/PROJECTS/bug-bounty-platform/backend/app/auth/dependencies.py new file mode 100644 index 00000000..312deafd --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/dependencies.py @@ -0,0 +1,21 @@ +""" +ⒸAngelaMos | 2025 +dependencies.py +""" + +from typing import Annotated + +from fastapi import Depends + +from core.dependencies import DBSession +from .service import AuthService + + +def get_auth_service(db: DBSession) -> AuthService: + """ + Dependency to inject AuthService instance + """ + return AuthService(db) + + +AuthServiceDep = Annotated[AuthService, Depends(get_auth_service)] diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/py.typed b/PROJECTS/bug-bounty-platform/backend/app/auth/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/repository.py b/PROJECTS/bug-bounty-platform/backend/app/auth/repository.py new file mode 100644 index 00000000..e97c04ce --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/repository.py @@ -0,0 +1,178 @@ +""" +ⒸAngelaMos | 2025 +repository.py +""" + +from uuid import UUID +from datetime import UTC, datetime + +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from .RefreshToken import RefreshToken +from core.base_repository import BaseRepository + + +class RefreshTokenRepository(BaseRepository[RefreshToken]): + """ + Repository for RefreshToken model database operations + """ + model = RefreshToken + + @classmethod + async def get_by_hash( + cls, + session: AsyncSession, + token_hash: str, + ) -> RefreshToken | None: + """ + Get refresh token by its hash + """ + result = await session.execute( + select(RefreshToken).where( + RefreshToken.token_hash == token_hash + ) + ) + return result.scalars().first() + + @classmethod + async def get_valid_by_hash( + cls, + session: AsyncSession, + token_hash: str, + ) -> RefreshToken | None: + """ + Get valid (not revoked, not expired) refresh token by hash + """ + result = await session.execute( + select(RefreshToken).where( + RefreshToken.token_hash == token_hash, + RefreshToken.is_revoked == False, + RefreshToken.expires_at > datetime.now(UTC), + ) + ) + return result.scalars().first() + + @classmethod + async def create_token( + cls, + session: AsyncSession, + user_id: UUID, + token_hash: str, + family_id: UUID, + expires_at: datetime, + device_id: str | None = None, + device_name: str | None = None, + ip_address: str | None = None, + ) -> RefreshToken: + """ + Create a new refresh token + """ + token = RefreshToken( + user_id = user_id, + token_hash = token_hash, + family_id = family_id, + expires_at = expires_at, + device_id = device_id, + device_name = device_name, + ip_address = ip_address, + ) + session.add(token) + await session.flush() + await session.refresh(token) + return token + + @classmethod + async def revoke_token( + cls, + session: AsyncSession, + token: RefreshToken, + ) -> RefreshToken: + """ + Revoke a single token + """ + token.revoke() + await session.flush() + await session.refresh(token) + return token + + @classmethod + async def revoke_family( + cls, + session: AsyncSession, + family_id: UUID, + ) -> int: + """ + Revoke all tokens in a family (for replay attack response) + + Returns count of revoked tokens + """ + result = await session.execute( + update(RefreshToken).where( + RefreshToken.family_id == family_id, + RefreshToken.is_revoked == False, + ).values(is_revoked = True, + revoked_at = datetime.now(UTC)) + ) + await session.flush() + return result.rowcount or 0 + + @classmethod + async def revoke_all_user_tokens( + cls, + session: AsyncSession, + user_id: UUID, + ) -> int: + """ + Revoke all tokens for a user (logout all devices) + + Returns count of revoked tokens + """ + result = await session.execute( + update(RefreshToken).where( + RefreshToken.user_id == user_id, + RefreshToken.is_revoked == False, + ).values(is_revoked = True, + revoked_at = datetime.now(UTC)) + ) + await session.flush() + return result.rowcount or 0 + + @classmethod + async def get_user_active_sessions( + cls, + session: AsyncSession, + user_id: UUID, + ) -> list[RefreshToken]: + """ + Get all active sessions for a user + """ + result = await session.execute( + select(RefreshToken).where( + RefreshToken.user_id == user_id, + RefreshToken.is_revoked == False, + RefreshToken.expires_at > datetime.now(UTC), + ) + ) + return list(result.scalars().all()) + + @classmethod + async def cleanup_expired( + cls, + session: AsyncSession, + ) -> int: + """ + Delete expired tokens (for maintenance job) + + Returns count of deleted tokens + """ + result = await session.execute( + select(RefreshToken).where( + RefreshToken.expires_at < datetime.now(UTC) + ) + ) + tokens = result.scalars().all() + for token in tokens: + await session.delete(token) + await session.flush() + return len(tokens) diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/routes.py b/PROJECTS/bug-bounty-platform/backend/app/auth/routes.py new file mode 100644 index 00000000..afbb10e3 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/routes.py @@ -0,0 +1,154 @@ +""" +ⒸAngelaMos | 2025 +routes.py +""" + +from typing import Annotated + +from fastapi import ( + APIRouter, + Cookie, + Depends, + Request, + Response, + status, +) +from fastapi.security import ( + OAuth2PasswordRequestForm, +) + +from config import settings +from core.dependencies import ( + ClientIP, + CurrentUser, +) +from core.security import ( + clear_refresh_cookie, + set_refresh_cookie, +) +from core.rate_limit import limiter +from core.exceptions import TokenError +from .schemas import ( + PasswordChange, + TokenResponse, + TokenWithUserResponse, +) +from user.schemas import UserResponse +from .dependencies import AuthServiceDep +from user.dependencies import UserServiceDep +from core.responses import AUTH_401 + + +router = APIRouter(prefix = "/auth", tags = ["auth"]) + + +@router.post( + "/login", + response_model = TokenWithUserResponse, + responses = {**AUTH_401} +) +@limiter.limit(settings.RATE_LIMIT_AUTH) +async def login( + request: Request, + response: Response, + auth_service: AuthServiceDep, + ip: ClientIP, + form_data: Annotated[OAuth2PasswordRequestForm, + Depends()], +) -> TokenWithUserResponse: + """ + Login with email and password + """ + result, refresh_token = await auth_service.login( + email=form_data.username, + password=form_data.password, + ip_address=ip, + ) + set_refresh_cookie(response, refresh_token) + return result + + +@router.post( + "/refresh", + response_model = TokenResponse, + responses = {**AUTH_401} +) +async def refresh_token( + response: Response, + auth_service: AuthServiceDep, + ip: ClientIP, + refresh_token: str | None = Cookie(None), +) -> TokenResponse: + """ + Refresh access token + """ + if not refresh_token: + raise TokenError("Refresh token required") + result, new_refresh_token = await auth_service.refresh_tokens( + refresh_token, + ip_address = ip + ) + set_refresh_cookie(response, new_refresh_token) + return result + + +@router.post( + "/logout", + status_code = status.HTTP_204_NO_CONTENT, + responses = {**AUTH_401} +) +async def logout( + response: Response, + auth_service: AuthServiceDep, + refresh_token: str | None = Cookie(None), +) -> None: + """ + Logout current session + """ + if not refresh_token: + raise TokenError("Refresh token required") + await auth_service.logout(refresh_token) + clear_refresh_cookie(response) + + +@router.post("/logout-all", responses = {**AUTH_401}) +async def logout_all( + response: Response, + auth_service: AuthServiceDep, + current_user: CurrentUser, +) -> dict[str, + int]: + """ + Logout from all devices + """ + count = await auth_service.logout_all(current_user) + clear_refresh_cookie(response) + return {"revoked_sessions": count} + + +@router.get("/me", response_model = UserResponse, responses = {**AUTH_401}) +async def get_current_user(current_user: CurrentUser) -> UserResponse: + """ + Get current authenticated user + """ + return UserResponse.model_validate(current_user) + + +@router.post( + "/change-password", + status_code = status.HTTP_204_NO_CONTENT, + responses = {**AUTH_401} +) +async def change_password( + user_service: UserServiceDep, + current_user: CurrentUser, + data: PasswordChange, +) -> None: + """ + Change current user password + """ + await user_service.change_password( + current_user, + data.current_password, + data.new_password, + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/schemas.py b/PROJECTS/bug-bounty-platform/backend/app/auth/schemas.py new file mode 100644 index 00000000..f5347924 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/schemas.py @@ -0,0 +1,78 @@ +""" +ⒸAngelaMos | 2025 +schemas.py +""" + +from pydantic import ( + Field, + EmailStr, +) + +from config import ( + PASSWORD_MAX_LENGTH, + PASSWORD_MIN_LENGTH, +) +from core.base_schema import BaseSchema +from user.schemas import UserResponse + + +class LoginRequest(BaseSchema): + """ + Schema for login request + """ + email: EmailStr + password: str = Field( + min_length = PASSWORD_MIN_LENGTH, + max_length = PASSWORD_MAX_LENGTH + ) + + +class TokenResponse(BaseSchema): + """ + Schema for token response + """ + access_token: str + token_type: str = "bearer" + + +class TokenWithUserResponse(TokenResponse): + """ + Schema for login response with user data + """ + user: UserResponse + + +class RefreshTokenRequest(BaseSchema): + """ + Schema for refresh token request via body + """ + refresh_token: str + + +class PasswordResetRequest(BaseSchema): + """ + Schema for password reset request + """ + email: EmailStr + + +class PasswordResetConfirm(BaseSchema): + """ + Schema for password reset confirmation + """ + token: str + new_password: str = Field( + min_length = PASSWORD_MIN_LENGTH, + max_length = PASSWORD_MAX_LENGTH + ) + + +class PasswordChange(BaseSchema): + """ + Schema for changing password while authenticated + """ + current_password: str + new_password: str = Field( + min_length = PASSWORD_MIN_LENGTH, + max_length = PASSWORD_MAX_LENGTH + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/auth/service.py b/PROJECTS/bug-bounty-platform/backend/app/auth/service.py new file mode 100644 index 00000000..6e710b24 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/auth/service.py @@ -0,0 +1,213 @@ +""" +ⒸAngelaMos | 2025 +service.py +""" + +import uuid6 +from sqlalchemy.ext.asyncio import ( + AsyncSession, +) + +from core.exceptions import ( + InvalidCredentials, + TokenError, + TokenRevokedError, +) +from core.security import ( + hash_token, + create_access_token, + create_refresh_token, + verify_password_with_timing_safety, +) +from user.User import User +from user.repository import UserRepository +from .repository import RefreshTokenRepository +from .schemas import ( + TokenResponse, + TokenWithUserResponse, +) +from user.schemas import UserResponse + + +class AuthService: + """ + Business logic for authentication operations + """ + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def authenticate( + self, + email: str, + password: str, + device_id: str | None = None, + device_name: str | None = None, + ip_address: str | None = None, + ) -> tuple[str, + str, + User]: + """ + Authenticate user and create tokens + """ + user = await UserRepository.get_by_email(self.session, email) + hashed_password = user.hashed_password if user else None + + is_valid, new_hash = await verify_password_with_timing_safety( + password, hashed_password + ) + + if not is_valid or user is None: + raise InvalidCredentials() + + if not user.is_active: + raise InvalidCredentials() + + if new_hash: + await UserRepository.update_password( + self.session, + user, + new_hash + ) + + access_token = create_access_token(user.id, user.token_version) + + family_id = uuid6.uuid7() + raw_refresh, token_hash, expires_at = create_refresh_token(user.id, family_id) + + await RefreshTokenRepository.create_token( + self.session, + user_id = user.id, + token_hash = token_hash, + family_id = family_id, + expires_at = expires_at, + device_id = device_id, + device_name = device_name, + ip_address = ip_address, + ) + + return access_token, raw_refresh, user + + async def login( + self, + email: str, + password: str, + device_id: str | None = None, + device_name: str | None = None, + ip_address: str | None = None, + ) -> tuple[TokenWithUserResponse, + str]: + """ + Login and return tokens with user data + """ + access_token, refresh_token, user = await self.authenticate( + email, + password, + device_id, + device_name, + ip_address, + ) + + response = TokenWithUserResponse( + access_token = access_token, + user = UserResponse.model_validate(user), + ) + return response, refresh_token + + async def refresh_tokens( + self, + refresh_token: str, + device_id: str | None = None, + device_name: str | None = None, + ip_address: str | None = None, + ) -> tuple[TokenResponse, + str]: + """ + Refresh access token using refresh token + + Implements token rotation with replay attack detection + """ + token_hash = hash_token(refresh_token) + stored_token = await RefreshTokenRepository.get_by_hash( + self.session, + token_hash + ) + + if stored_token is None: + raise TokenError(message = "Invalid refresh token") + + if stored_token.is_revoked: + await RefreshTokenRepository.revoke_family( + self.session, + stored_token.family_id + ) + raise TokenRevokedError() + + if stored_token.is_expired: + raise TokenError(message = "Refresh token expired") + + user = await UserRepository.get_by_id( + self.session, + stored_token.user_id + ) + if user is None or not user.is_active: + raise TokenError(message = "User not found or inactive") + + await RefreshTokenRepository.revoke_token( + self.session, + stored_token + ) + + access_token = create_access_token(user.id, user.token_version) + + new_raw_token, new_hash, expires_at = create_refresh_token( + user.id, stored_token.family_id + ) + + await RefreshTokenRepository.create_token( + self.session, + user_id = user.id, + token_hash = new_hash, + family_id = stored_token.family_id, + expires_at = expires_at, + device_id = device_id, + device_name = device_name, + ip_address = ip_address, + ) + + return TokenResponse(access_token = access_token), new_raw_token + + async def logout( + self, + refresh_token: str, + ) -> None: + """ + Logout by revoking refresh token + + Silently succeeds if token is already revoked or doesn't exist + """ + token_hash = hash_token(refresh_token) + stored_token = await RefreshTokenRepository.get_by_hash( + self.session, + token_hash + ) + + if stored_token and not stored_token.is_revoked: + await RefreshTokenRepository.revoke_token( + self.session, + stored_token + ) + + async def logout_all( + self, + user: User, + ) -> int: + """ + Logout from all devices + + Returns count of revoked sessions + """ + await UserRepository.increment_token_version(self.session, user) + return await RefreshTokenRepository.revoke_all_user_tokens( + self.session, + user.id + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/config.py b/PROJECTS/bug-bounty-platform/backend/app/config.py new file mode 100644 index 00000000..75f34345 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/config.py @@ -0,0 +1,213 @@ +""" +ⒸAngelaMos | 2026 +config.py +""" + +from pathlib import Path +from typing import Literal +from functools import lru_cache + +from pydantic import ( + EmailStr, + Field, + RedisDsn, + SecretStr, + PostgresDsn, + model_validator, +) +from pydantic_settings import ( + BaseSettings, + SettingsConfigDict, +) + +from core.constants import ( + API_PREFIX, + API_VERSION, + ASSET_DESCRIPTION_MAX_LENGTH, + ASSET_IDENTIFIER_MAX_LENGTH, + BIO_MAX_LENGTH, + COMMENT_MAX_LENGTH, + COMPANY_NAME_MAX_LENGTH, + CURRENCY_MAX_LENGTH, + CWE_ID_MAX_LENGTH, + DEVICE_ID_MAX_LENGTH, + DEVICE_NAME_MAX_LENGTH, + EMAIL_MAX_LENGTH, + FILENAME_MAX_LENGTH, + FULL_NAME_MAX_LENGTH, + IP_ADDRESS_MAX_LENGTH, + MIME_TYPE_MAX_LENGTH, + PASSWORD_HASH_MAX_LENGTH, + PASSWORD_MAX_LENGTH, + PASSWORD_MIN_LENGTH, + PROGRAM_DESCRIPTION_MAX_LENGTH, + PROGRAM_NAME_MAX_LENGTH, + PROGRAM_RULES_MAX_LENGTH, + PROGRAM_SLUG_MAX_LENGTH, + REPORT_DESCRIPTION_MAX_LENGTH, + REPORT_IMPACT_MAX_LENGTH, + REPORT_STEPS_MAX_LENGTH, + REPORT_TITLE_MAX_LENGTH, + STORAGE_PATH_MAX_LENGTH, + TOKEN_HASH_LENGTH, + WEBSITE_MAX_LENGTH, +) +from core.enums import ( + AssetType, + Environment, + HealthStatus, + ProgramStatus, + ProgramVisibility, + ReportStatus, + SafeEnum, + Severity, + TokenType, + UserRole, +) + + +__all__ = [ + "API_PREFIX", + "API_VERSION", + "ASSET_DESCRIPTION_MAX_LENGTH", + "ASSET_IDENTIFIER_MAX_LENGTH", + "BIO_MAX_LENGTH", + "COMMENT_MAX_LENGTH", + "COMPANY_NAME_MAX_LENGTH", + "CURRENCY_MAX_LENGTH", + "CWE_ID_MAX_LENGTH", + "DEVICE_ID_MAX_LENGTH", + "DEVICE_NAME_MAX_LENGTH", + "EMAIL_MAX_LENGTH", + "FILENAME_MAX_LENGTH", + "FULL_NAME_MAX_LENGTH", + "IP_ADDRESS_MAX_LENGTH", + "MIME_TYPE_MAX_LENGTH", + "PASSWORD_HASH_MAX_LENGTH", + "PASSWORD_MAX_LENGTH", + "PASSWORD_MIN_LENGTH", + "PROGRAM_DESCRIPTION_MAX_LENGTH", + "PROGRAM_NAME_MAX_LENGTH", + "PROGRAM_RULES_MAX_LENGTH", + "PROGRAM_SLUG_MAX_LENGTH", + "REPORT_DESCRIPTION_MAX_LENGTH", + "REPORT_IMPACT_MAX_LENGTH", + "REPORT_STEPS_MAX_LENGTH", + "REPORT_TITLE_MAX_LENGTH", + "STORAGE_PATH_MAX_LENGTH", + "TOKEN_HASH_LENGTH", + "WEBSITE_MAX_LENGTH", + "AssetType", + "Environment", + "HealthStatus", + "ProgramStatus", + "ProgramVisibility", + "ReportStatus", + "SafeEnum", + "Settings", + "Severity", + "TokenType", + "UserRole", + "get_settings", + "settings", +] + +_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +_ENV_FILE = _PROJECT_ROOT / ".env" + + +class Settings(BaseSettings): + """ + Application settings loaded from environment variables + """ + model_config = SettingsConfigDict( + env_file = _ENV_FILE, + env_file_encoding = "utf-8", + case_sensitive = False, + extra = "ignore", + ) + + APP_NAME: str = "bug-bounty-platform" + APP_VERSION: str = "1.0.0" + APP_SUMMARY: str = "Developed CarterPerez-dev" + APP_DESCRIPTION: str = "FastAPI async first boilerplate - JWT, Asyncdb, PostgreSQL" + APP_CONTACT_NAME: str = "AngelaMos LLC" + APP_CONTACT_EMAIL: str = "support@certgames.com" + APP_LICENSE_NAME: str = "MIT" + APP_LICENSE_URL: str = "https://github.com/CarterPerez-dev/Cybersecurity-Projects/blob/main/LICENSE" + + ENVIRONMENT: Environment = Environment.DEVELOPMENT + DEBUG: bool = False + + HOST: str = "0.0.0.0" + PORT: int = 8000 + RELOAD: bool = True + + DATABASE_URL: PostgresDsn + DB_POOL_SIZE: int = Field(default = 20, ge = 5, le = 100) + DB_MAX_OVERFLOW: int = Field(default = 10, ge = 0, le = 50) + DB_POOL_TIMEOUT: int = Field(default = 30, ge = 10) + DB_POOL_RECYCLE: int = Field(default = 1800, ge = 300) + + SECRET_KEY: SecretStr = Field(..., min_length = 32) + JWT_ALGORITHM: Literal["HS256", "HS384", "HS512"] = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default = 15, ge = 5, le = 60) + REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default = 7, ge = 1, le = 30) + + ADMIN_EMAIL: EmailStr | None = None + + REDIS_URL: RedisDsn | None = None + + CORS_ORIGINS: list[str] = [ + "http://localhost", + "http://localhost:3420", + "http://localhost:8420", + ] + CORS_ALLOW_CREDENTIALS: bool = True + CORS_ALLOW_METHODS: list[str] = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS" + ] + CORS_ALLOW_HEADERS: list[str] = ["*"] + + RATE_LIMIT_DEFAULT: str = "100/minute" + RATE_LIMIT_AUTH: str = "20/minute" + + PAGINATION_DEFAULT_SIZE: int = Field(default = 20, ge = 1, le = 100) + PAGINATION_MAX_SIZE: int = Field(default = 100, ge = 1, le = 500) + + LOG_LEVEL: Literal["DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL"] = "INFO" + LOG_JSON_FORMAT: bool = True + + @model_validator(mode = "after") + def validate_production_settings(self) -> "Settings": + """ + Enforce security constraints in production environment. + """ + if self.ENVIRONMENT == Environment.PRODUCTION: + if self.DEBUG: + raise ValueError("DEBUG must be False in production") + if self.CORS_ORIGINS == ["*"]: + raise ValueError( + "CORS_ORIGINS cannot be ['*'] in production" + ) + return self + + +@lru_cache +def get_settings() -> Settings: + """ + Cached settings instance to avoid repeated env parsing + """ + return Settings() + + +settings = get_settings() diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/Base.py b/PROJECTS/bug-bounty-platform/backend/app/core/Base.py new file mode 100644 index 00000000..e5f2f80e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/Base.py @@ -0,0 +1,84 @@ +""" +ⒸAngelaMos | 2025 +Base.py +""" + +from uuid import UUID +from datetime import UTC, datetime + +import uuid6 +from sqlalchemy.orm import ( + Mapped, + mapped_column, + DeclarativeBase, +) +from sqlalchemy import ( + DateTime, + MetaData, + func, +) +from sqlalchemy.ext.asyncio import AsyncAttrs + + +NAMING_CONVENTION = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} + + +class Base(AsyncAttrs, DeclarativeBase): + """ + Base class for all SQLAlchemy models + """ + metadata = MetaData(naming_convention = NAMING_CONVENTION) + + +class UUIDMixin: + """ + Mixin for UUID v7 primary key + + UUID v7 is time sortable and distributed safe, optimal for B tree indexes + """ + id: Mapped[UUID] = mapped_column( + primary_key = True, + default = uuid6.uuid7, + ) + + +class TimestampMixin: + """ + Mixin for created_at and updated_at timestamps + """ + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone = True), + default = lambda: datetime.now(UTC), + server_default = func.now(), + ) + updated_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone = True), + default = None, + onupdate = lambda: datetime.now(UTC), + server_onupdate = func.now(), + ) + + +class SoftDeleteMixin: + """ + Mixin for soft delete functionality + """ + deleted_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone = True), + default = None, + ) + is_deleted: Mapped[bool] = mapped_column(default = False) + + def soft_delete(self) -> None: + self.is_deleted = True + self.deleted_at = datetime.now(UTC) + + def restore(self) -> None: + self.is_deleted = False + self.deleted_at = None diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/core/__init__.py new file mode 100644 index 00000000..e863581c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/__init__.py @@ -0,0 +1,4 @@ +""" +ⒸAngelaMos | 2025 +__init__.py +""" diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/base_repository.py b/PROJECTS/bug-bounty-platform/backend/app/core/base_repository.py new file mode 100644 index 00000000..030ea6a5 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/base_repository.py @@ -0,0 +1,106 @@ +""" +ⒸAngelaMos | 2025 +base_repository.py +""" + +from collections.abc import Sequence +from typing import ( + Any, + Generic, + TypeVar, +) +from uuid import UUID + +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from .Base import Base + + +ModelT = TypeVar("ModelT", bound = Base) + + +class BaseRepository(Generic[ModelT]): + """ + Generic repository with common CRUD operations + """ + model: type[ModelT] + + @classmethod + async def get_by_id( + cls, + session: AsyncSession, + id: UUID, + ) -> ModelT | None: + """ + Get a single record by ID + """ + return await session.get(cls.model, id) + + @classmethod + async def get_multi( + cls, + session: AsyncSession, + skip: int = 0, + limit: int = 100, + ) -> Sequence[ModelT]: + """ + Get multiple records with pagination + """ + result = await session.execute( + select(cls.model).offset(skip).limit(limit) + ) + return result.scalars().all() + + @classmethod + async def count(cls, session: AsyncSession) -> int: + """ + Count total records + """ + result = await session.execute( + select(func.count()).select_from(cls.model) + ) + return result.scalar_one() + + @classmethod + async def create( + cls, + session: AsyncSession, + **kwargs: Any, + ) -> ModelT: + """ + Create a new record + """ + instance = cls.model(**kwargs) + session.add(instance) + await session.flush() + await session.refresh(instance) + return instance + + @classmethod + async def update( + cls, + session: AsyncSession, + instance: ModelT, + **kwargs: Any, + ) -> ModelT: + """ + Update an existing record + """ + for key, value in kwargs.items(): + setattr(instance, key, value) + await session.flush() + await session.refresh(instance) + return instance + + @classmethod + async def delete( + cls, + session: AsyncSession, + instance: ModelT, + ) -> None: + """ + Delete a record + """ + await session.delete(instance) + await session.flush() diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/base_schema.py b/PROJECTS/bug-bounty-platform/backend/app/core/base_schema.py new file mode 100644 index 00000000..183e7d6c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/base_schema.py @@ -0,0 +1,50 @@ +""" +ⒸAngelaMos | 2025 +base.py +""" + +from typing import Any +from collections.abc import Callable +from decimal import Decimal +from uuid import UUID +from datetime import datetime + +from pydantic import ( + BaseModel, + ConfigDict, + field_serializer, +) + + +class BaseSchema(BaseModel): + """ + Base schema with common configuration + """ + model_config = ConfigDict( + from_attributes = True, + str_strip_whitespace = True, + ) + + @field_serializer('*', mode = 'wrap', when_used = 'json') + def serialize_decimals( + self, + value: Any, + nxt: Callable[[Any], + Any], + _info: Any, + ) -> Any: + """ + Serialize Decimal fields as float for JSON compatibility + """ + if isinstance(value, Decimal): + return float(value) + return nxt(value) + + +class BaseResponseSchema(BaseSchema): + """ + Base schema for API responses with common fields + """ + id: UUID + created_at: datetime + updated_at: datetime | None = None diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/common_schemas.py b/PROJECTS/bug-bounty-platform/backend/app/core/common_schemas.py new file mode 100644 index 00000000..c47029bd --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/common_schemas.py @@ -0,0 +1,34 @@ +""" +ⒸAngelaMos | 2025 +common_schemas.py +""" + +from config import HealthStatus +from .base_schema import BaseSchema + + +class HealthResponse(BaseSchema): + """ + Health check response + """ + status: HealthStatus + environment: str + version: str + + +class HealthDetailedResponse(HealthResponse): + """ + Detailed health check with component status + """ + database: HealthStatus + redis: HealthStatus | None = None + + +class AppInfoResponse(BaseSchema): + """ + Root endpoint response with API information + """ + name: str + version: str + environment: str + docs_url: str | None diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/constants.py b/PROJECTS/bug-bounty-platform/backend/app/core/constants.py new file mode 100644 index 00000000..7be8b006 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/constants.py @@ -0,0 +1,44 @@ +""" +ⒸAngelaMos | 2025 +constants.py +""" + +EMAIL_MAX_LENGTH = 320 +PASSWORD_MIN_LENGTH = 8 +PASSWORD_MAX_LENGTH = 128 +PASSWORD_HASH_MAX_LENGTH = 1024 +FULL_NAME_MAX_LENGTH = 255 + +TOKEN_HASH_LENGTH = 64 +DEVICE_ID_MAX_LENGTH = 255 +DEVICE_NAME_MAX_LENGTH = 100 +IP_ADDRESS_MAX_LENGTH = 45 + +API_VERSION = "v1" +API_PREFIX = f"/{API_VERSION}" + +COMPANY_NAME_MAX_LENGTH = 255 +BIO_MAX_LENGTH = 2000 +WEBSITE_MAX_LENGTH = 500 + +PROGRAM_NAME_MAX_LENGTH = 255 +PROGRAM_SLUG_MAX_LENGTH = 100 +PROGRAM_DESCRIPTION_MAX_LENGTH = 10000 +PROGRAM_RULES_MAX_LENGTH = 50000 + +ASSET_IDENTIFIER_MAX_LENGTH = 500 +ASSET_DESCRIPTION_MAX_LENGTH = 2000 + +CURRENCY_MAX_LENGTH = 3 + +REPORT_TITLE_MAX_LENGTH = 500 +REPORT_DESCRIPTION_MAX_LENGTH = 50000 +REPORT_STEPS_MAX_LENGTH = 50000 +REPORT_IMPACT_MAX_LENGTH = 10000 +CWE_ID_MAX_LENGTH = 20 + +COMMENT_MAX_LENGTH = 20000 + +FILENAME_MAX_LENGTH = 255 +STORAGE_PATH_MAX_LENGTH = 500 +MIME_TYPE_MAX_LENGTH = 100 diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/database.py b/PROJECTS/bug-bounty-platform/backend/app/core/database.py new file mode 100644 index 00000000..03ffeef1 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/database.py @@ -0,0 +1,160 @@ +""" +ⒸAngelaMos | 2025 +database.py +""" + +import contextlib +from collections.abc import ( + AsyncIterator, + Iterator, +) + +from sqlalchemy import create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.engine.url import make_url +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + AsyncConnection, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import Session, sessionmaker + +from config import settings + + +class DatabaseSessionManager: + """ + Manages database connections and sessions for both sync and async contexts + """ + def __init__(self) -> None: + self._async_engine: AsyncEngine | None = None + self._sync_engine: Engine | None = None + self._async_sessionmaker: async_sessionmaker[AsyncSession + ] | None = None + self._sync_sessionmaker: sessionmaker[Session] | None = None + + def init(self, database_url: str) -> None: + """ + Initialize database engines and session factories + """ + base_url = make_url(database_url) + + async_url = base_url.set(drivername = "postgresql+asyncpg") + self._async_engine = create_async_engine( + async_url, + pool_size = settings.DB_POOL_SIZE, + max_overflow = settings.DB_MAX_OVERFLOW, + pool_timeout = settings.DB_POOL_TIMEOUT, + pool_recycle = settings.DB_POOL_RECYCLE, + pool_pre_ping = True, + echo = settings.DEBUG, + ) + self._async_sessionmaker = async_sessionmaker( + bind = self._async_engine, + class_ = AsyncSession, + autocommit = False, + autoflush = False, + expire_on_commit = False, + ) + + sync_url = base_url.set(drivername = "postgresql+psycopg2") + self._sync_engine = create_engine( + sync_url, + pool_size = settings.DB_POOL_SIZE, + max_overflow = settings.DB_MAX_OVERFLOW, + pool_timeout = settings.DB_POOL_TIMEOUT, + pool_recycle = settings.DB_POOL_RECYCLE, + pool_pre_ping = True, + echo = settings.DEBUG, + ) + self._sync_sessionmaker = sessionmaker( + bind = self._sync_engine, + autocommit = False, + autoflush = False, + expire_on_commit = False, + ) + + async def close(self) -> None: + """ + Dispose of all database connections + """ + if self._async_engine: + await self._async_engine.dispose() + self._async_engine = None + self._async_sessionmaker = None + + if self._sync_engine: + self._sync_engine.dispose() + self._sync_engine = None + self._sync_sessionmaker = None + + @contextlib.asynccontextmanager + async def session(self) -> AsyncIterator[AsyncSession]: + """ + Async context manager for database sessions + + Handles commit on success, rollback on exception + """ + if self._async_sessionmaker is None: + raise RuntimeError("DatabaseSessionManager is not initialized") + + session = self._async_sessionmaker() + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + @contextlib.asynccontextmanager + async def connect(self) -> AsyncIterator[AsyncConnection]: + """ + Async context manager for raw database connections + """ + if self._async_engine is None: + raise RuntimeError("DatabaseSessionManager is not initialized") + + async with self._async_engine.begin() as connection: + yield connection + + @property + def sync_engine(self) -> Engine: + """ + Sync engine for Alembic migrations + """ + if self._sync_engine is None: + raise RuntimeError("DatabaseSessionManager is not initialized") + return self._sync_engine + + @contextlib.contextmanager + def sync_session(self) -> Iterator[Session]: + """ + Sync context manager for migrations and CLI tools + """ + if self._sync_sessionmaker is None: + raise RuntimeError("DatabaseSessionManager is not initialized") + + session = self._sync_sessionmaker() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + +sessionmanager = DatabaseSessionManager() + + +async def get_db_session() -> AsyncIterator[AsyncSession]: + """ + FastAPI dependency for database sessions + """ + async with sessionmanager.session() as session: + yield session diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/dependencies.py b/PROJECTS/bug-bounty-platform/backend/app/core/dependencies.py new file mode 100644 index 00000000..dfa80969 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/dependencies.py @@ -0,0 +1,146 @@ +""" +ⒸAngelaMos | 2025 +dependencies.py +""" + +from __future__ import annotations + +from typing import Annotated +from uuid import UUID + +import jwt +from fastapi import Depends, Request +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession + +from config import ( + API_PREFIX, + TokenType, + UserRole, +) +from .database import get_db_session +from .exceptions import ( + InactiveUser, + PermissionDenied, + TokenError, + TokenRevokedError, + UserNotFound, +) +from user.User import User +from .security import decode_access_token +from user.repository import UserRepository + + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl = f"{API_PREFIX}/auth/login", + auto_error = True, +) + +oauth2_scheme_optional = OAuth2PasswordBearer( + tokenUrl = f"{API_PREFIX}/auth/login", + auto_error = False, +) + +DBSession = Annotated[AsyncSession, Depends(get_db_session)] + + +async def get_current_user( + token: Annotated[str, + Depends(oauth2_scheme)], + db: DBSession, +) -> User: + """ + Validate access token and return current user + """ + try: + payload = decode_access_token(token) + except jwt.InvalidTokenError as e: + raise TokenError(message = str(e)) from e + + if payload.get("type") != TokenType.ACCESS.value: + raise TokenError(message = "Invalid token type") + + user_id = UUID(payload["sub"]) + user = await UserRepository.get_by_id(db, user_id) + + if user is None: + raise UserNotFound(identifier = str(user_id)) + + if payload.get("token_version") != user.token_version: + raise TokenRevokedError() + + return user + + +async def get_current_active_user( + user: Annotated[User, + Depends(get_current_user)], +) -> User: + """ + Ensure user is active + """ + if not user.is_active: + raise InactiveUser() + return user + + +async def get_optional_user( + token: Annotated[str | None, + Depends(oauth2_scheme_optional)], + db: DBSession, +) -> User | None: + """ + Return current user if authenticated, None otherwise + """ + if token is None: + return None + + try: + payload = decode_access_token(token) + if payload.get("type") != TokenType.ACCESS.value: + return None + user_id = UUID(payload["sub"]) + user = await UserRepository.get_by_id(db, user_id) + if user and user.token_version == payload.get("token_version"): + return user + except (jwt.InvalidTokenError, ValueError): + pass + + return None + + +class RequireRole: + """ + Dependency class to check user role + """ + def __init__(self, *allowed_roles: UserRole) -> None: + self.allowed_roles = allowed_roles + + async def __call__( + self, + user: Annotated[User, + Depends(get_current_active_user)], + ) -> User: + if user.role not in self.allowed_roles: + raise PermissionDenied( + message = + f"Requires one of roles: {', '.join(r.value for r in self.allowed_roles)}", + ) + return user + + +CurrentUser = Annotated["User", Depends(get_current_active_user)] +OptionalUser = Annotated["User | None", Depends(get_optional_user)] + + +def get_client_ip(request: Request) -> str: + """ + Extract client IP considering proxy headers + """ + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",")[0].strip() + return request.client.host if request.client else "unknown" + + +ClientIP = Annotated[str, Depends(get_client_ip)] diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/enums.py b/PROJECTS/bug-bounty-platform/backend/app/core/enums.py new file mode 100644 index 00000000..1b56a8a1 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/enums.py @@ -0,0 +1,139 @@ +""" +ⒸAngelaMos | 2025 +enums.py +""" + +from enum import Enum +from typing import Any + +import sqlalchemy as sa + + +def enum_values_callable(enum_class: type[Enum]) -> list[str]: + """ + Returns enum VALUES (not names) for SQLAlchemy storage + + Prevents the common trap where SQLAlchemy stores enum NAMES by default, + causing database breakage if you rename an enum member + """ + return [str(item.value) for item in enum_class] + + +class SafeEnum(sa.Enum): + """ + SQLAlchemy Enum type that stores VALUES and handles unknown values gracefully + + https://blog.wrouesnel.com/posts/sqlalchemy-enums-careful-what-goes-into-the-database/ + """ + def __init__(self, *enums: type[Enum], **kw: Any) -> None: + if "values_callable" not in kw: + kw["values_callable"] = enum_values_callable + super().__init__(*enums, **kw) + self._unknown_value = ( + kw["_adapted_from"]._unknown_value + if "_adapted_from" in kw else kw.get("unknown_value") + ) + + def _object_value_for_elem(self, elem: str) -> Enum: + """ + Override to return unknown_value instead of raising LookupError + """ + try: + return self._object_lookup[elem] + except LookupError: + if self._unknown_value is not None: + return self._unknown_value + raise + + +class Environment(str, Enum): + """ + Application environment. + """ + DEVELOPMENT = "development" + STAGING = "staging" + PRODUCTION = "production" + + +class UserRole(str, Enum): + """ + User roles for authorization. + """ + UNKNOWN = "unknown" + USER = "user" + COMPANY = "company" + ADMIN = "admin" + + +class TokenType(str, Enum): + """ + JWT token types. + """ + ACCESS = "access" + REFRESH = "refresh" + + +class HealthStatus(str, Enum): + """ + Health check status values. + """ + HEALTHY = "healthy" + UNHEALTHY = "unhealthy" + DEGRADED = "degraded" + + +class ProgramStatus(str, Enum): + """ + Bug bounty program lifecycle status. + """ + DRAFT = "draft" + ACTIVE = "active" + PAUSED = "paused" + CLOSED = "closed" + + +class ProgramVisibility(str, Enum): + """ + Bug bounty program visibility level. + """ + PUBLIC = "public" + PRIVATE = "private" + INVITE_ONLY = "invite_only" + + +class AssetType(str, Enum): + """ + Type of asset in a bug bounty program scope. + """ + DOMAIN = "domain" + API = "api" + MOBILE_APP = "mobile_app" + SOURCE_CODE = "source_code" + HARDWARE = "hardware" + OTHER = "other" + + +class Severity(str, Enum): + """ + Vulnerability severity levels aligned with CVSS. + """ + CRITICAL = "critical" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + INFORMATIONAL = "informational" + + +class ReportStatus(str, Enum): + """ + Vulnerability report lifecycle status. + """ + NEW = "new" + TRIAGING = "triaging" + NEEDS_MORE_INFO = "needs_more_info" + ACCEPTED = "accepted" + DUPLICATE = "duplicate" + INFORMATIVE = "informative" + NOT_APPLICABLE = "not_applicable" + RESOLVED = "resolved" + DISCLOSED = "disclosed" diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/error_schemas.py b/PROJECTS/bug-bounty-platform/backend/app/core/error_schemas.py new file mode 100644 index 00000000..28795432 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/error_schemas.py @@ -0,0 +1,27 @@ +""" +ⒸAngelaMos | 2025 +errors.py +""" + +from typing import ClassVar +from pydantic import Field, ConfigDict +from core.base_schema import BaseSchema + + +class ErrorDetail(BaseSchema): + """ + Standard error response format + """ + detail: str = Field(..., description = "Human readable error message") + type: str = Field(..., description = "Exception class name") + + model_config: ClassVar[ConfigDict] = ConfigDict( + json_schema_extra = { + "examples": [ + { + "detail": "User with id '123' not found", + "type": "UserNotFound" + } + ] + } + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/exceptions.py b/PROJECTS/bug-bounty-platform/backend/app/core/exceptions.py new file mode 100644 index 00000000..480b764c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/exceptions.py @@ -0,0 +1,340 @@ +""" +ⒸAngelaMos | 2025 +exceptions.py +""" + +from typing import Any + + +class BaseAppException(Exception): + """ + Base exception for all application specific errors + """ + def __init__( + self, + message: str, + status_code: int = 500, + extra: dict[str, + Any] | None = None, + ) -> None: + self.message = message + self.status_code = status_code + self.extra = extra or {} + super().__init__(self.message) + + +class ResourceNotFound(BaseAppException): + """ + Raised when a requested resource does not exist + """ + def __init__( + self, + resource: str, + identifier: str | int, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + message = f"{resource} with id '{identifier}' not found", + status_code = 404, + extra = extra, + ) + self.resource = resource + self.identifier = identifier + + +class ConflictError(BaseAppException): + """ + Raised when an operation conflicts with existing state + """ + def __init__( + self, + message: str, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + message = message, + status_code = 409, + extra = extra + ) + + +class ValidationError(BaseAppException): + """ + Raised when input validation fails outside of Pydantic + """ + def __init__( + self, + message: str, + field: str | None = None, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + message = message, + status_code = 422, + extra = extra + ) + self.field = field + + +class AuthenticationError(BaseAppException): + """ + Raised when authentication fails + """ + def __init__( + self, + message: str = "Authentication failed", + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + message = message, + status_code = 401, + extra = extra + ) + + +class TokenError(AuthenticationError): + """ + Raised for JWT token specific errors + """ + def __init__( + self, + message: str = "Invalid or expired token", + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__(message = message, extra = extra) + + +class TokenRevokedError(TokenError): + """ + Raised when a revoked token is used + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__(message = "Token has been revoked", extra = extra) + + +class PermissionDenied(BaseAppException): + """ + Raised when user lacks required permissions + """ + def __init__( + self, + message: str = "Permission denied", + required_permission: str | None = None, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + message = message, + status_code = 403, + extra = extra + ) + self.required_permission = required_permission + + +class RateLimitExceeded(BaseAppException): + """ + Raised when rate limit is exceeded + """ + def __init__( + self, + message: str = "Calm down a little bit...", + retry_after: int | None = None, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + message = message, + status_code = 420, + extra = extra + ) + self.retry_after = retry_after + + +class UserNotFound(ResourceNotFound): + """ + Raised when a user is not found + """ + def __init__( + self, + identifier: str | int, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + resource = "User", + identifier = identifier, + extra = extra + ) + + +class EmailAlreadyExists(ConflictError): + """ + Raised when attempting to register with an existing email + """ + def __init__( + self, + email: str, + extra: dict[str, + Any] | None = None + ) -> None: + super().__init__( + message = f"Email '{email}' is already registered", + extra = extra, + ) + self.email = email + + +class InvalidCredentials(AuthenticationError): + """ + Raised when login credentials are invalid + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__( + message = "Invalid email or password", + extra = extra + ) + + +class InactiveUser(AuthenticationError): + """ + Raised when an inactive user attempts to authenticate. + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__( + message = "User account is inactive", + extra = extra + ) + + +class ProgramNotFound(ResourceNotFound): + """ + Raised when a program is not found + """ + def __init__( + self, + identifier: str | int, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + resource = "Program", + identifier = identifier, + extra = extra + ) + + +class SlugAlreadyExists(ConflictError): + """ + Raised when attempting to create a program with existing slug + """ + def __init__( + self, + slug: str, + extra: dict[str, + Any] | None = None + ) -> None: + super().__init__( + message = f"Program with slug '{slug}' already exists", + extra = extra, + ) + self.slug = slug + + +class AssetNotFound(ResourceNotFound): + """ + Raised when an asset is not found + """ + def __init__( + self, + identifier: str | int, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + resource = "Asset", + identifier = identifier, + extra = extra + ) + + +class NotProgramOwner(PermissionDenied): + """ + Raised when user tries to modify a program they don't own + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__( + message = "You are not the owner of this program", + extra = extra + ) + + +class ReportNotFound(ResourceNotFound): + """ + Raised when a report is not found + """ + def __init__( + self, + identifier: str | int, + extra: dict[str, + Any] | None = None, + ) -> None: + super().__init__( + resource = "Report", + identifier = identifier, + extra = extra + ) + + +class NotReportOwner(PermissionDenied): + """ + Raised when user tries to modify a report they don't own + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__( + message = "You are not the owner of this report", + extra = extra + ) + + +class ProgramNotActive(ValidationError): + """ + Raised when trying to submit to an inactive program + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__( + message = "This program is not accepting submissions", + extra = extra + ) + + +class CannotSubmitToOwnProgram(ValidationError): + """ + Raised when user tries to submit a report to their own program + """ + def __init__(self, extra: dict[str, Any] | None = None) -> None: + super().__init__( + message = "You cannot submit reports to your own program", + extra = extra + ) + + +class InvalidStatusTransition(ValidationError): + """ + Raised when an invalid status transition is attempted + """ + def __init__( + self, + current: str, + target: str, + extra: dict[str, + Any] | None = None + ) -> None: + super().__init__( + message = f"Cannot transition from '{current}' to '{target}'", + extra = extra + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/health_routes.py b/PROJECTS/bug-bounty-platform/backend/app/core/health_routes.py new file mode 100644 index 00000000..b02a8ee6 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/health_routes.py @@ -0,0 +1,79 @@ +""" +ⒸAngelaMos | 2025 +health_routes.py +""" + +from fastapi import ( + APIRouter, + status, +) +import redis.asyncio as redis +from sqlalchemy import text + +from config import ( + settings, + HealthStatus, +) +from .common_schemas import ( + HealthResponse, + HealthDetailedResponse, +) +from .database import sessionmanager + + +router = APIRouter(tags = ["health"]) + + +@router.get( + "/health", + response_model = HealthResponse, + status_code = status.HTTP_200_OK, +) +async def health_check() -> HealthResponse: + """ + Basic health check + """ + return HealthResponse( + status = HealthStatus.HEALTHY, + environment = settings.ENVIRONMENT.value, + version = settings.APP_VERSION, + ) + + +@router.get( + "/health/detailed", + response_model = HealthDetailedResponse, + status_code = status.HTTP_200_OK, +) +async def health_check_detailed() -> HealthDetailedResponse: + """ + Detailed health check including database connectivity + """ + db_status = HealthStatus.UNHEALTHY + redis_status = None + + try: + async with sessionmanager.connect() as conn: + await conn.execute(text("SELECT 1")) + db_status = HealthStatus.HEALTHY + except Exception: + db_status = HealthStatus.UNHEALTHY + + if settings.REDIS_URL: + try: + r = redis.from_url(str(settings.REDIS_URL)) + await r.ping() + redis_status = HealthStatus.HEALTHY + await r.close() + except Exception: + redis_status = HealthStatus.UNHEALTHY + + overall = HealthStatus.HEALTHY if db_status == HealthStatus.HEALTHY else HealthStatus.DEGRADED + + return HealthDetailedResponse( + status = overall, + environment = settings.ENVIRONMENT.value, + version = settings.APP_VERSION, + database = db_status, + redis = redis_status, + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/logging.py b/PROJECTS/bug-bounty-platform/backend/app/core/logging.py new file mode 100644 index 00000000..515ac3f7 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/logging.py @@ -0,0 +1,79 @@ +""" +ⒸAngelaMos | 2025 +logging.py +""" + +import logging +import sys + +import structlog +from structlog.types import Processor + +from config import ( + settings, + Environment, +) + + +def configure_logging() -> None: + """ + Structlog with appropriate processors for the environment + """ + shared_processors: list[Processor] = [ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + structlog.stdlib.add_logger_name, + structlog.processors.TimeStamper(fmt = "iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.UnicodeDecoder(), + ] + + if settings.ENVIRONMENT == Environment.PRODUCTION: + shared_processors.append(structlog.processors.format_exc_info) + renderer: Processor = structlog.processors.JSONRenderer() + else: + renderer = structlog.dev.ConsoleRenderer(colors = True) + + structlog.configure( + processors = shared_processors + [ + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, + ], + logger_factory = structlog.stdlib.LoggerFactory(), + wrapper_class = structlog.stdlib.BoundLogger, + cache_logger_on_first_use = True, + ) + + formatter = structlog.stdlib.ProcessorFormatter( + foreign_pre_chain = shared_processors, + processors = [ + structlog.stdlib.ProcessorFormatter.remove_processors_meta, + renderer, + ], + ) + + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + + root_logger = logging.getLogger() + root_logger.handlers.clear() + root_logger.addHandler(handler) + root_logger.setLevel(settings.LOG_LEVEL) + + for logger_name in ["uvicorn", + "uvicorn.access", + "uvicorn.error", + "sqlalchemy.engine"]: + logger = logging.getLogger(logger_name) + logger.handlers.clear() + logger.addHandler(handler) + logger.propagate = False + + if settings.ENVIRONMENT != Environment.DEVELOPMENT: + logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) + + +def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger: + """ + Get a structured logger instance + """ + return structlog.get_logger(name) diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/py.typed b/PROJECTS/bug-bounty-platform/backend/app/core/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/rate_limit.py b/PROJECTS/bug-bounty-platform/backend/app/core/rate_limit.py new file mode 100644 index 00000000..34efe551 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/rate_limit.py @@ -0,0 +1,44 @@ +""" +ⒸAngelaMos | 2025 +rate_limit.py +""" + +import jwt +from slowapi import Limiter +from slowapi.util import get_remote_address +from starlette.requests import Request + +from config import settings + + +def get_identifier(request: Request) -> str: + """ + Get rate limit identifier + + Uses user ID if authenticated, otherwise falls back to IP address + (Will add more fingerprinting if needed depending on project) + """ + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + try: + token = auth_header.split(" ")[1] + payload = jwt.decode( + token, + options = {"verify_signature": False}, + ) + user_id = payload.get("sub") + if user_id: + return f"user:{user_id}" + except Exception: + pass + + return get_remote_address(request) + + +limiter = Limiter( + key_func = get_identifier, + storage_uri = str(settings.REDIS_URL) if settings.REDIS_URL else None, + default_limits = [settings.RATE_LIMIT_DEFAULT], + headers_enabled = True, + in_memory_fallback_enabled = True, +) diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/responses.py b/PROJECTS/bug-bounty-platform/backend/app/core/responses.py new file mode 100644 index 00000000..4f7b0132 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/responses.py @@ -0,0 +1,45 @@ +""" +ⒸAngelaMos | 2025 +responses.py +""" + +from typing import Any + +from .error_schemas import ErrorDetail + + +AUTH_401: dict[int | str, + dict[str, + Any]] = { + 401: { + "model": ErrorDetail, + "description": "Authentication failed" + }, + } + +FORBIDDEN_403: dict[int | str, + dict[str, + Any]] = { + 403: { + "model": ErrorDetail, + "description": "Permission denied" + }, + } + +NOT_FOUND_404: dict[int | str, + dict[str, + Any]] = { + 404: { + "model": ErrorDetail, + "description": "Resource not found" + }, + } + +CONFLICT_409: dict[int | str, + dict[str, + Any]] = { + 409: { + "model": ErrorDetail, + "description": "Resource conflict" + }, + } diff --git a/PROJECTS/bug-bounty-platform/backend/app/core/security.py b/PROJECTS/bug-bounty-platform/backend/app/core/security.py new file mode 100644 index 00000000..76c36c4f --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/core/security.py @@ -0,0 +1,190 @@ +""" +ⒸAngelaMos | 2025 +security.py +""" + +import asyncio +import hashlib +import secrets +from datetime import ( + UTC, + datetime, + timedelta, +) +from typing import Any +from uuid import UUID + +import jwt +from fastapi import Response +from pwdlib import PasswordHash + +from config import ( + settings, + TokenType, +) + + +password_hasher = PasswordHash.recommended() + + +async def hash_password(password: str) -> str: + """ + Hash password using Argon2id + + Runs in thread pool to avoid blocking the async event loop + since Argon2 is CPU intensive by design + """ + return await asyncio.to_thread(password_hasher.hash, password) + + +async def verify_password(plain_password: str, + hashed_password: str) -> tuple[bool, + str | None]: + """ + Verify password and check if rehash is needed + + Returns: + Tuple of (is_valid, new_hash_if_needs_rehash) + If password is valid but hash params are outdated, returns new hash + """ + try: + return await asyncio.to_thread( + password_hasher.verify_and_update, + plain_password, + hashed_password + ) + except Exception: + return False, None + + +DUMMY_HASH = password_hasher.hash( + "dummy_password_for_timing_attack_prevention" +) + + +async def verify_password_with_timing_safety( + plain_password: str, + hashed_password: str | None, +) -> tuple[bool, + str | None]: + """ + Verify password with constant time behavior to prevent user enumeration + + If no hash is provided (user doesn't exist), still performs a dummy + hash operation to prevent timing attacks + """ + if hashed_password is None: + await asyncio.to_thread( + password_hasher.verify, + plain_password, + DUMMY_HASH + ) + return False, None + return await verify_password(plain_password, hashed_password) + + +def create_access_token( + user_id: UUID, + token_version: int, + extra_claims: dict[str, + Any] | None = None, +) -> str: + """ + Create a short lived access token + """ + now = datetime.now(UTC) + payload = { + "sub": str(user_id), + "type": TokenType.ACCESS.value, + "token_version": token_version, + "iat": now, + "exp": + now + timedelta(minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES), + } + if extra_claims: + payload.update(extra_claims) + + return jwt.encode( + payload, + settings.SECRET_KEY.get_secret_value(), + algorithm = settings.JWT_ALGORITHM, + ) + + +def create_refresh_token( + user_id: UUID, + family_id: UUID, +) -> tuple[str, + str, + datetime]: + """ + Create a long lived refresh token + + Returns: + Tuple of (raw_token, token_hash, expires_at) + Raw token is sent to client, hash is stored in database + """ + raw_token = secrets.token_urlsafe(32) + token_hash = hashlib.sha256(raw_token.encode()).hexdigest() + expires_at = datetime.now(UTC) + timedelta( + days = settings.REFRESH_TOKEN_EXPIRE_DAYS + ) + + return raw_token, token_hash, expires_at + + +def decode_access_token(token: str) -> dict[str, Any]: + """ + Decode and validate an access token + + Raises: + jwt.InvalidTokenError: If token is invalid or expired + """ + return jwt.decode( + token, + settings.SECRET_KEY.get_secret_value(), + algorithms = [settings.JWT_ALGORITHM], + options = { + "require": ["exp", + "sub", + "iat", + "type", + "token_version"] + }, + ) + + +def hash_token(token: str) -> str: + """ + Hash a token for secure storage + """ + return hashlib.sha256(token.encode()).hexdigest() + + +def generate_secure_token(nbytes: int = 32) -> str: + """ + Generate a cryptographically secure random token + """ + return secrets.token_urlsafe(nbytes) + + +def set_refresh_cookie(response: Response, token: str) -> None: + """ + Set refresh token as HttpOnly cookie + """ + response.set_cookie( + key = "refresh_token", + value = token, + httponly = True, + secure = settings.ENVIRONMENT.value != "development", + samesite = "strict", + max_age = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, + path = "/", + ) + + +def clear_refresh_cookie(response: Response) -> None: + """ + Clear refresh token cookie + """ + response.delete_cookie(key = "refresh_token", path = "/") diff --git a/PROJECTS/bug-bounty-platform/backend/app/factory.py b/PROJECTS/bug-bounty-platform/backend/app/factory.py new file mode 100644 index 00000000..c8f790e4 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/factory.py @@ -0,0 +1,145 @@ +""" +ⒸAngelaMos | 2025 +factory.py +""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded + +from config import settings, Environment, API_PREFIX +from core.database import sessionmanager +from core.exceptions import BaseAppException +from core.logging import configure_logging +from core.rate_limit import limiter +from middleware.correlation import CorrelationIdMiddleware +from core.common_schemas import AppInfoResponse +from core.health_routes import router as health_router +from user.routes import router as user_router +from auth.routes import router as auth_router +from admin.routes import router as admin_router +from program.routes import router as program_router +from report.routes import router as report_router + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + """ + Application lifespan handler for startup and shutdown + """ + configure_logging() + sessionmanager.init(str(settings.DATABASE_URL)) + yield + await sessionmanager.close() + + +OPENAPI_TAGS = [ + { + "name": "root", + "description": "API information" + }, + { + "name": "health", + "description": "Health check endpoints" + }, + { + "name": "auth", + "description": "Authentication and authorization" + }, + { + "name": "users", + "description": "User registration and profile management" + }, + { + "name": "programs", + "description": "Bug bounty program management" + }, + { + "name": "reports", + "description": "Vulnerability report submission and triage" + }, + { + "name": "admin", + "description": "Admin only operations" + }, +] + + +def create_app() -> FastAPI: + """ + Application factory + """ + is_production = settings.ENVIRONMENT == Environment.PRODUCTION + + app = FastAPI( + title = settings.APP_NAME, + summary = settings.APP_SUMMARY, + description = settings.APP_DESCRIPTION, + version = settings.APP_VERSION, + contact = { + "name": settings.APP_CONTACT_NAME, + "email": settings.APP_CONTACT_EMAIL, + }, + license_info = { + "name": settings.APP_LICENSE_NAME, + "url": settings.APP_LICENSE_URL, + }, + openapi_tags = OPENAPI_TAGS, + openapi_version = "3.1.0", + lifespan = lifespan, + root_path = "/api", + openapi_url = "/openapi.json", + docs_url = "/docs", + redoc_url = "/redoc", + ) + + app.add_middleware(CorrelationIdMiddleware) + app.add_middleware( + CORSMiddleware, + allow_origins = settings.CORS_ORIGINS, + allow_credentials = settings.CORS_ALLOW_CREDENTIALS, + allow_methods = settings.CORS_ALLOW_METHODS, + allow_headers = settings.CORS_ALLOW_HEADERS, + ) + + app.state.limiter = limiter + app.add_exception_handler( + RateLimitExceeded, + _rate_limit_exceeded_handler + ) + + @app.exception_handler(BaseAppException) + async def app_exception_handler( + request: Request, + exc: BaseAppException, + ) -> JSONResponse: + return JSONResponse( + status_code = exc.status_code, + content = { + "detail": exc.message, + "type": exc.__class__.__name__, + }, + ) + + @app.get("/", response_model = AppInfoResponse, tags = ["root"]) + async def root() -> AppInfoResponse: + return AppInfoResponse( + name = settings.APP_NAME, + version = settings.APP_VERSION, + environment = settings.ENVIRONMENT.value, + docs_url = "/docs", + ) + + app.include_router(health_router) + app.include_router(admin_router, prefix = API_PREFIX) + app.include_router(auth_router, prefix = API_PREFIX) + app.include_router(user_router, prefix = API_PREFIX) + app.include_router(program_router, prefix = API_PREFIX) + app.include_router(report_router, prefix = API_PREFIX) + + return app diff --git a/PROJECTS/bug-bounty-platform/backend/app/middleware/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/middleware/__init__.py new file mode 100644 index 00000000..f50bdec5 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/middleware/__init__.py @@ -0,0 +1,11 @@ +""" +AngelaMos | 2025 +__init__.py +""" + +from .correlation import CorrelationIdMiddleware + + +__all__ = [ + "CorrelationIdMiddleware", +] diff --git a/PROJECTS/bug-bounty-platform/backend/app/middleware/correlation.py b/PROJECTS/bug-bounty-platform/backend/app/middleware/correlation.py new file mode 100644 index 00000000..7f64addb --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/middleware/correlation.py @@ -0,0 +1,43 @@ +""" +ⒸAngelaMos | 2025 +correlation.py +""" + +import uuid +from collections.abc import ( + Awaitable, + Callable, +) +import structlog +from starlette.requests import Request +from starlette.responses import Response +from starlette.middleware.base import BaseHTTPMiddleware + + +RequestResponseEndpoint = Callable[[Request], Awaitable[Response]] + + +class CorrelationIdMiddleware(BaseHTTPMiddleware): + """ + Correlation ID to requests for distributed tracing + """ + async def dispatch( + self, + request: Request, + call_next: RequestResponseEndpoint, + ) -> Response: + correlation_id = request.headers.get( + "X-Correlation-ID", + str(uuid.uuid4()) + ) + + structlog.contextvars.clear_contextvars() + structlog.contextvars.bind_contextvars( + correlation_id = correlation_id, + method = request.method, + path = request.url.path, + ) + + response = await call_next(request) + response.headers["X-Correlation-ID"] = correlation_id + return response diff --git a/PROJECTS/bug-bounty-platform/backend/app/middleware/py.typed b/PROJECTS/bug-bounty-platform/backend/app/middleware/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/Asset.py b/PROJECTS/bug-bounty-platform/backend/app/program/Asset.py new file mode 100644 index 00000000..ef0c3b0e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/Asset.py @@ -0,0 +1,64 @@ +""" +ⒸAngelaMos | 2025 +Asset.py +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import ForeignKey, String, Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + ASSET_IDENTIFIER_MAX_LENGTH, + AssetType, + SafeEnum, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from program.Program import Program + + +class Asset(Base, UUIDMixin, TimestampMixin): + """ + Target asset within a bug bounty program scope + """ + __tablename__ = "assets" + + program_id: Mapped[UUID] = mapped_column( + ForeignKey("programs.id", + ondelete = "CASCADE"), + index = True, + ) + + asset_type: Mapped[AssetType] = mapped_column( + SafeEnum(AssetType), + default = AssetType.DOMAIN, + ) + + identifier: Mapped[str] = mapped_column( + String(ASSET_IDENTIFIER_MAX_LENGTH), + ) + + in_scope: Mapped[bool] = mapped_column(default = True) + + description: Mapped[str | None] = mapped_column( + Text, + default = None, + ) + + program: Mapped[Program] = relationship( + back_populates = "assets", + lazy = "raise", + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/Program.py b/PROJECTS/bug-bounty-platform/backend/app/program/Program.py new file mode 100644 index 00000000..735c7e9a --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/Program.py @@ -0,0 +1,114 @@ +""" +ⒸAngelaMos | 2025 +Program.py +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import ForeignKey, String, Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + PROGRAM_NAME_MAX_LENGTH, + PROGRAM_SLUG_MAX_LENGTH, + ProgramStatus, + ProgramVisibility, + SafeEnum, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from program.Asset import Asset + from program.RewardTier import RewardTier + from report.Report import Report + from user.User import User + + +class Program(Base, UUIDMixin, TimestampMixin): + """ + Bug bounty program hosted by a company or individual + """ + __tablename__ = "programs" + + company_id: Mapped[UUID] = mapped_column( + ForeignKey("users.id", + ondelete = "CASCADE"), + index = True, + ) + + name: Mapped[str] = mapped_column( + String(PROGRAM_NAME_MAX_LENGTH), + ) + slug: Mapped[str] = mapped_column( + String(PROGRAM_SLUG_MAX_LENGTH), + unique = True, + index = True, + ) + description: Mapped[str | None] = mapped_column( + Text, + default = None, + ) + rules: Mapped[str | None] = mapped_column( + Text, + default = None, + ) + + response_sla_hours: Mapped[int] = mapped_column(default = 72) + + status: Mapped[ProgramStatus] = mapped_column( + SafeEnum(ProgramStatus), + default = ProgramStatus.DRAFT, + index = True, + ) + visibility: Mapped[ProgramVisibility] = mapped_column( + SafeEnum(ProgramVisibility), + default = ProgramVisibility.PUBLIC, + ) + + company: Mapped[User] = relationship( + back_populates = "programs", + lazy = "raise", + ) + + assets: Mapped[list[Asset]] = relationship( + back_populates = "program", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + reward_tiers: Mapped[list[RewardTier]] = relationship( + back_populates = "program", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + reports: Mapped[list[Report]] = relationship( + back_populates = "program", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + @property + def is_active(self) -> bool: + """ + Check if program is accepting submissions + """ + return self.status == ProgramStatus.ACTIVE + + @property + def is_public(self) -> bool: + """ + Check if program is publicly visible + """ + return self.visibility == ProgramVisibility.PUBLIC diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/RewardTier.py b/PROJECTS/bug-bounty-platform/backend/app/program/RewardTier.py new file mode 100644 index 00000000..c84b4a83 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/RewardTier.py @@ -0,0 +1,60 @@ +""" +ⒸAngelaMos | 2025 +RewardTier.py +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + CURRENCY_MAX_LENGTH, + SafeEnum, + Severity, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from program.Program import Program + + +class RewardTier(Base, UUIDMixin, TimestampMixin): + """ + Bounty reward tier by severity for a program + """ + __tablename__ = "reward_tiers" + + program_id: Mapped[UUID] = mapped_column( + ForeignKey("programs.id", + ondelete = "CASCADE"), + index = True, + ) + + severity: Mapped[Severity] = mapped_column( + SafeEnum(Severity), + ) + + min_bounty: Mapped[int] = mapped_column(default = 0) + max_bounty: Mapped[int] = mapped_column(default = 0) + + currency: Mapped[str] = mapped_column( + String(CURRENCY_MAX_LENGTH), + default = "USD", + ) + + program: Mapped[Program] = relationship( + back_populates = "reward_tiers", + lazy = "raise", + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/program/__init__.py new file mode 100644 index 00000000..e863581c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/__init__.py @@ -0,0 +1,4 @@ +""" +ⒸAngelaMos | 2025 +__init__.py +""" diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/dependencies.py b/PROJECTS/bug-bounty-platform/backend/app/program/dependencies.py new file mode 100644 index 00000000..4f72cb88 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/dependencies.py @@ -0,0 +1,21 @@ +""" +ⒸAngelaMos | 2025 +dependencies.py +""" + +from typing import Annotated + +from fastapi import Depends + +from core.dependencies import DBSession +from .service import ProgramService + + +def get_program_service(db: DBSession) -> ProgramService: + """ + Dependency to inject ProgramService instance + """ + return ProgramService(db) + + +ProgramServiceDep = Annotated[ProgramService, Depends(get_program_service)] diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/repository.py b/PROJECTS/bug-bounty-platform/backend/app/program/repository.py new file mode 100644 index 00000000..18b72aa8 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/repository.py @@ -0,0 +1,232 @@ +""" +ⒸAngelaMos | 2025 +repository.py +""" + +from collections.abc import Sequence +from uuid import UUID + +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from config import ProgramStatus, ProgramVisibility +from core.base_repository import BaseRepository +from .Program import Program +from .Asset import Asset +from .RewardTier import RewardTier + + +class ProgramRepository(BaseRepository[Program]): + """ + Repository for Program model database operations + """ + model = Program + + @classmethod + async def get_by_slug( + cls, + session: AsyncSession, + slug: str, + ) -> Program | None: + """ + Get program by slug + """ + result = await session.execute( + select(Program).where(Program.slug == slug) + ) + return result.scalars().first() + + @classmethod + async def get_by_slug_with_details( + cls, + session: AsyncSession, + slug: str, + ) -> Program | None: + """ + Get program by slug with assets and reward tiers + """ + result = await session.execute( + select(Program).where(Program.slug == slug).options( + selectinload(Program.assets), + selectinload(Program.reward_tiers), + ) + ) + return result.scalars().first() + + @classmethod + async def get_by_id_with_details( + cls, + session: AsyncSession, + program_id: UUID, + ) -> Program | None: + """ + Get program by ID with assets and reward tiers + """ + result = await session.execute( + select(Program).where(Program.id == program_id).options( + selectinload(Program.assets), + selectinload(Program.reward_tiers), + ) + ) + return result.scalars().first() + + @classmethod + async def slug_exists( + cls, + session: AsyncSession, + slug: str, + ) -> bool: + """ + Check if slug is already taken + """ + result = await session.execute( + select(Program.id).where(Program.slug == slug) + ) + return result.scalars().first() is not None + + @classmethod + async def get_public_programs( + cls, + session: AsyncSession, + skip: int = 0, + limit: int = 20, + ) -> Sequence[Program]: + """ + Get active public programs + """ + result = await session.execute( + select(Program).where( + Program.status == ProgramStatus.ACTIVE, + Program.visibility == ProgramVisibility.PUBLIC, + ).order_by(Program.created_at.desc() + ).offset(skip).limit(limit) + ) + return result.scalars().all() + + @classmethod + async def count_public_programs( + cls, + session: AsyncSession, + ) -> int: + """ + Count active public programs + """ + result = await session.execute( + select(func.count()).select_from(Program).where( + Program.status == ProgramStatus.ACTIVE, + Program.visibility == ProgramVisibility.PUBLIC, + ) + ) + return result.scalar_one() + + @classmethod + async def get_by_company( + cls, + session: AsyncSession, + company_id: UUID, + skip: int = 0, + limit: int = 20, + ) -> Sequence[Program]: + """ + Get programs by company/owner + """ + result = await session.execute( + select(Program).where(Program.company_id == company_id + ).order_by(Program.created_at.desc() + ).offset(skip).limit(limit) + ) + return result.scalars().all() + + @classmethod + async def count_by_company( + cls, + session: AsyncSession, + company_id: UUID, + ) -> int: + """ + Count programs by company + """ + result = await session.execute( + select(func.count()).select_from(Program).where( + Program.company_id == company_id + ) + ) + return result.scalar_one() + + +class AssetRepository(BaseRepository[Asset]): + """ + Repository for Asset model database operations + """ + model = Asset + + @classmethod + async def get_by_program( + cls, + session: AsyncSession, + program_id: UUID, + ) -> Sequence[Asset]: + """ + Get all assets for a program + """ + result = await session.execute( + select(Asset).where( + Asset.program_id == program_id + ).order_by(Asset.in_scope.desc(), + Asset.created_at) + ) + return result.scalars().all() + + +class RewardTierRepository(BaseRepository[RewardTier]): + """ + Repository for RewardTier model database operations + """ + model = RewardTier + + @classmethod + async def get_by_program( + cls, + session: AsyncSession, + program_id: UUID, + ) -> Sequence[RewardTier]: + """ + Get all reward tiers for a program + """ + result = await session.execute( + select(RewardTier).where(RewardTier.program_id == program_id) + ) + return result.scalars().all() + + @classmethod + async def get_by_program_and_severity( + cls, + session: AsyncSession, + program_id: UUID, + severity: str, + ) -> RewardTier | None: + """ + Get reward tier by program and severity + """ + result = await session.execute( + select(RewardTier).where( + RewardTier.program_id == program_id, + RewardTier.severity == severity, + ) + ) + return result.scalars().first() + + @classmethod + async def delete_by_program( + cls, + session: AsyncSession, + program_id: UUID, + ) -> None: + """ + Delete all reward tiers for a program + """ + tiers = await cls.get_by_program(session, program_id) + for tier in tiers: + await session.delete(tier) + await session.flush() diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/routes.py b/PROJECTS/bug-bounty-platform/backend/app/program/routes.py new file mode 100644 index 00000000..08b1b470 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/routes.py @@ -0,0 +1,279 @@ +""" +ⒸAngelaMos | 2025 +routes.py +""" + +from uuid import UUID + +from fastapi import APIRouter, Query, status + +from core.dependencies import CurrentUser +from core.responses import ( + AUTH_401, + CONFLICT_409, + FORBIDDEN_403, + NOT_FOUND_404, +) +from .schemas import ( + AssetCreate, + AssetResponse, + AssetUpdate, + ProgramCreate, + ProgramDetailResponse, + ProgramListResponse, + ProgramResponse, + ProgramUpdate, + RewardTierCreate, + RewardTierResponse, +) +from .dependencies import ProgramServiceDep + + +router = APIRouter(prefix = "/programs", tags = ["programs"]) + + +@router.post( + "", + response_model = ProgramResponse, + status_code = status.HTTP_201_CREATED, + responses = { + **AUTH_401, + **CONFLICT_409 + }, +) +async def create_program( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_data: ProgramCreate, +) -> ProgramResponse: + """ + Create a new bug bounty program + """ + return await program_service.create_program(current_user, program_data) + + +@router.get( + "", + response_model = ProgramListResponse, +) +async def list_programs( + program_service: ProgramServiceDep, + page: int = Query(default = 1, + ge = 1), + size: int = Query(default = 20, + ge = 1, + le = 100), +) -> ProgramListResponse: + """ + List active public programs + """ + return await program_service.list_public_programs(page, size) + + +@router.get( + "/mine", + response_model = ProgramListResponse, + responses = {**AUTH_401}, +) +async def list_my_programs( + program_service: ProgramServiceDep, + current_user: CurrentUser, + page: int = Query(default = 1, + ge = 1), + size: int = Query(default = 20, + ge = 1, + le = 100), +) -> ProgramListResponse: + """ + List programs owned by current user + """ + return await program_service.list_my_programs(current_user, page, size) + + +@router.get( + "/{slug}", + response_model = ProgramDetailResponse, + responses = {**NOT_FOUND_404}, +) +async def get_program( + program_service: ProgramServiceDep, + slug: str, +) -> ProgramDetailResponse: + """ + Get program by slug with full details + """ + return await program_service.get_program_by_slug(slug) + + +@router.patch( + "/{program_id}", + response_model = ProgramResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def update_program( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_id: UUID, + program_data: ProgramUpdate, +) -> ProgramResponse: + """ + Update program details + """ + return await program_service.update_program( + current_user, + program_id, + program_data + ) + + +@router.delete( + "/{program_id}", + status_code = status.HTTP_204_NO_CONTENT, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def delete_program( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_id: UUID, +) -> None: + """ + Delete a program + """ + await program_service.delete_program(current_user, program_id) + + +@router.get( + "/{program_id}/assets", + response_model = list[AssetResponse], + responses = {**NOT_FOUND_404}, +) +async def list_assets( + program_service: ProgramServiceDep, + program_id: UUID, +) -> list[AssetResponse]: + """ + List program assets (scope) + """ + return await program_service.list_assets(program_id) + + +@router.post( + "/{program_id}/assets", + response_model = AssetResponse, + status_code = status.HTTP_201_CREATED, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def add_asset( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_id: UUID, + asset_data: AssetCreate, +) -> AssetResponse: + """ + Add asset to program scope + """ + return await program_service.add_asset( + current_user, + program_id, + asset_data + ) + + +@router.patch( + "/{program_id}/assets/{asset_id}", + response_model = AssetResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def update_asset( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_id: UUID, + asset_id: UUID, + asset_data: AssetUpdate, +) -> AssetResponse: + """ + Update an asset + """ + return await program_service.update_asset( + current_user, + program_id, + asset_id, + asset_data + ) + + +@router.delete( + "/{program_id}/assets/{asset_id}", + status_code = status.HTTP_204_NO_CONTENT, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def delete_asset( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_id: UUID, + asset_id: UUID, +) -> None: + """ + Delete an asset + """ + await program_service.delete_asset(current_user, program_id, asset_id) + + +@router.get( + "/{program_id}/rewards", + response_model = list[RewardTierResponse], + responses = {**NOT_FOUND_404}, +) +async def list_reward_tiers( + program_service: ProgramServiceDep, + program_id: UUID, +) -> list[RewardTierResponse]: + """ + List program reward tiers + """ + return await program_service.list_reward_tiers(program_id) + + +@router.put( + "/{program_id}/rewards", + response_model = list[RewardTierResponse], + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def set_reward_tiers( + program_service: ProgramServiceDep, + current_user: CurrentUser, + program_id: UUID, + tiers: list[RewardTierCreate], +) -> list[RewardTierResponse]: + """ + Set reward tiers for program (replaces existing) + """ + return await program_service.set_reward_tiers( + current_user, + program_id, + tiers + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/schemas.py b/PROJECTS/bug-bounty-platform/backend/app/program/schemas.py new file mode 100644 index 00000000..e9edf427 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/schemas.py @@ -0,0 +1,179 @@ +""" +ⒸAngelaMos | 2025 +schemas.py +""" + +from uuid import UUID +from decimal import Decimal + +from pydantic import Field, field_validator, ValidationInfo + +from config import ( + ASSET_IDENTIFIER_MAX_LENGTH, + AssetType, + PROGRAM_DESCRIPTION_MAX_LENGTH, + PROGRAM_NAME_MAX_LENGTH, + PROGRAM_RULES_MAX_LENGTH, + PROGRAM_SLUG_MAX_LENGTH, + ProgramStatus, + ProgramVisibility, + Severity, +) +from core.base_schema import ( + BaseSchema, + BaseResponseSchema, +) + + +class RewardTierCreate(BaseSchema): + """ + Schema for creating a reward tier + """ + severity: Severity + min_bounty: int = Field(ge = 0, default = 0) + max_bounty: int = Field(ge = 0, default = 0) + currency: str = Field(default = "USD", max_length = 3) + + @field_validator("max_bounty") + @classmethod + def max_gte_min(cls, v: int, info: ValidationInfo) -> int: + """ + Ensure max bounty is greater than or equal to min bounty + """ + if "min_bounty" in info.data and v < info.data["min_bounty"]: + raise ValueError("max_bounty must be >= min_bounty") + return v + + +class RewardTierResponse(BaseResponseSchema): + """ + Schema for reward tier API responses + """ + program_id: UUID + severity: Severity + min_bounty: int + max_bounty: int + currency: str + + +class AssetCreate(BaseSchema): + """ + Schema for creating an asset + """ + asset_type: AssetType = AssetType.DOMAIN + identifier: str = Field(max_length = ASSET_IDENTIFIER_MAX_LENGTH) + in_scope: bool = True + description: str | None = None + + +class AssetUpdate(BaseSchema): + """ + Schema for updating an asset + """ + asset_type: AssetType | None = None + identifier: str | None = Field( + default = None, + max_length = ASSET_IDENTIFIER_MAX_LENGTH + ) + in_scope: bool | None = None + description: str | None = None + + +class AssetResponse(BaseResponseSchema): + """ + Schema for asset API responses + """ + program_id: UUID + asset_type: AssetType + identifier: str + in_scope: bool + description: str | None + + +class ProgramCreate(BaseSchema): + """ + Schema for creating a program + """ + name: str = Field(max_length = PROGRAM_NAME_MAX_LENGTH) + slug: str = Field( + max_length = PROGRAM_SLUG_MAX_LENGTH, + pattern = r"^[a-z0-9-]+$" + ) + description: str | None = Field( + default = None, + max_length = PROGRAM_DESCRIPTION_MAX_LENGTH + ) + rules: str | None = Field( + default = None, + max_length = PROGRAM_RULES_MAX_LENGTH + ) + response_sla_hours: int = Field(default = 72, ge = 1, le = 720) + visibility: ProgramVisibility = ProgramVisibility.PUBLIC + + +class ProgramUpdate(BaseSchema): + """ + Schema for updating a program + """ + name: str | None = Field( + default = None, + max_length = PROGRAM_NAME_MAX_LENGTH + ) + description: str | None = Field( + default = None, + max_length = PROGRAM_DESCRIPTION_MAX_LENGTH + ) + rules: str | None = Field( + default = None, + max_length = PROGRAM_RULES_MAX_LENGTH + ) + response_sla_hours: int | None = Field( + default = None, + ge = 1, + le = 720 + ) + status: ProgramStatus | None = None + visibility: ProgramVisibility | None = None + + +class ProgramResponse(BaseResponseSchema): + """ + Schema for program API responses + """ + company_id: UUID + name: str + slug: str + description: str | None + rules: str | None + response_sla_hours: int + status: ProgramStatus + visibility: ProgramVisibility + + +class ProgramDetailResponse(ProgramResponse): + """ + Schema for program detail with assets and rewards + """ + assets: list[AssetResponse] + reward_tiers: list[RewardTierResponse] + + +class ProgramListResponse(BaseSchema): + """ + Schema for paginated program list + """ + items: list[ProgramResponse] + total: int + page: int + size: int + + +class ProgramStatsResponse(BaseSchema): + """ + Schema for program statistics + """ + total_reports: int + open_reports: int + resolved_reports: int + total_paid: int + average_response_hours: Decimal | None diff --git a/PROJECTS/bug-bounty-platform/backend/app/program/service.py b/PROJECTS/bug-bounty-platform/backend/app/program/service.py new file mode 100644 index 00000000..dc48a651 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/program/service.py @@ -0,0 +1,375 @@ +""" +ⒸAngelaMos | 2025 +service.py +""" + +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession + +from core.exceptions import ( + AssetNotFound, + NotProgramOwner, + ProgramNotFound, + SlugAlreadyExists, +) +from user.User import User +from .schemas import ( + AssetCreate, + AssetResponse, + AssetUpdate, + ProgramCreate, + ProgramDetailResponse, + ProgramListResponse, + ProgramResponse, + ProgramUpdate, + RewardTierCreate, + RewardTierResponse, +) +from .repository import ( + AssetRepository, + ProgramRepository, + RewardTierRepository, +) + + +class ProgramService: + """ + Business logic for program operations + """ + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def create_program( + self, + user: User, + program_data: ProgramCreate, + ) -> ProgramResponse: + """ + Create a new bug bounty program + """ + if await ProgramRepository.slug_exists(self.session, + program_data.slug): + raise SlugAlreadyExists(program_data.slug) + + program = await ProgramRepository.create( + self.session, + company_id = user.id, + name = program_data.name, + slug = program_data.slug, + description = program_data.description, + rules = program_data.rules, + response_sla_hours = program_data.response_sla_hours, + visibility = program_data.visibility, + ) + return ProgramResponse.model_validate(program) + + async def get_program_by_slug( + self, + slug: str, + ) -> ProgramDetailResponse: + """ + Get program by slug with full details + """ + program = await ProgramRepository.get_by_slug_with_details( + self.session, + slug + ) + if not program: + raise ProgramNotFound(slug) + + return ProgramDetailResponse( + id = program.id, + created_at = program.created_at, + updated_at = program.updated_at, + company_id = program.company_id, + name = program.name, + slug = program.slug, + description = program.description, + rules = program.rules, + response_sla_hours = program.response_sla_hours, + status = program.status, + visibility = program.visibility, + assets = [ + AssetResponse.model_validate(a) for a in program.assets + ], + reward_tiers = [ + RewardTierResponse.model_validate(r) + for r in program.reward_tiers + ], + ) + + async def get_program_by_id( + self, + program_id: UUID, + ) -> ProgramResponse: + """ + Get program by ID + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + return ProgramResponse.model_validate(program) + + async def list_public_programs( + self, + page: int, + size: int, + ) -> ProgramListResponse: + """ + List active public programs + """ + skip = (page - 1) * size + programs = await ProgramRepository.get_public_programs( + self.session, + skip = skip, + limit = size, + ) + total = await ProgramRepository.count_public_programs(self.session) + return ProgramListResponse( + items = [ProgramResponse.model_validate(p) for p in programs], + total = total, + page = page, + size = size, + ) + + async def list_my_programs( + self, + user: User, + page: int, + size: int, + ) -> ProgramListResponse: + """ + List programs owned by user + """ + skip = (page - 1) * size + programs = await ProgramRepository.get_by_company( + self.session, + company_id = user.id, + skip = skip, + limit = size, + ) + total = await ProgramRepository.count_by_company( + self.session, + user.id + ) + return ProgramListResponse( + items = [ProgramResponse.model_validate(p) for p in programs], + total = total, + page = page, + size = size, + ) + + async def update_program( + self, + user: User, + program_id: UUID, + program_data: ProgramUpdate, + ) -> ProgramResponse: + """ + Update program details + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + update_dict = program_data.model_dump(exclude_unset = True) + updated = await ProgramRepository.update( + self.session, + program, + **update_dict, + ) + return ProgramResponse.model_validate(updated) + + async def delete_program( + self, + user: User, + program_id: UUID, + ) -> None: + """ + Delete a program + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + await ProgramRepository.delete(self.session, program) + + async def add_asset( + self, + user: User, + program_id: UUID, + asset_data: AssetCreate, + ) -> AssetResponse: + """ + Add asset to program scope + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + asset = await AssetRepository.create( + self.session, + program_id = program_id, + asset_type = asset_data.asset_type, + identifier = asset_data.identifier, + in_scope = asset_data.in_scope, + description = asset_data.description, + ) + return AssetResponse.model_validate(asset) + + async def update_asset( + self, + user: User, + program_id: UUID, + asset_id: UUID, + asset_data: AssetUpdate, + ) -> AssetResponse: + """ + Update an asset + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + asset = await AssetRepository.get_by_id(self.session, asset_id) + if not asset or asset.program_id != program_id: + raise AssetNotFound(str(asset_id)) + + update_dict = asset_data.model_dump(exclude_unset = True) + updated = await AssetRepository.update( + self.session, + asset, + **update_dict, + ) + return AssetResponse.model_validate(updated) + + async def delete_asset( + self, + user: User, + program_id: UUID, + asset_id: UUID, + ) -> None: + """ + Delete an asset + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + asset = await AssetRepository.get_by_id(self.session, asset_id) + if not asset or asset.program_id != program_id: + raise AssetNotFound(str(asset_id)) + + await AssetRepository.delete(self.session, asset) + + async def list_assets( + self, + program_id: UUID, + ) -> list[AssetResponse]: + """ + List all assets for a program + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + assets = await AssetRepository.get_by_program( + self.session, + program_id + ) + return [AssetResponse.model_validate(a) for a in assets] + + async def set_reward_tiers( + self, + user: User, + program_id: UUID, + tiers: list[RewardTierCreate], + ) -> list[RewardTierResponse]: + """ + Set reward tiers for a program (replaces existing) + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + await RewardTierRepository.delete_by_program( + self.session, + program_id + ) + + created_tiers = [] + for tier_data in tiers: + tier = await RewardTierRepository.create( + self.session, + program_id = program_id, + severity = tier_data.severity, + min_bounty = tier_data.min_bounty, + max_bounty = tier_data.max_bounty, + currency = tier_data.currency, + ) + created_tiers.append(RewardTierResponse.model_validate(tier)) + + return created_tiers + + async def list_reward_tiers( + self, + program_id: UUID, + ) -> list[RewardTierResponse]: + """ + List reward tiers for a program + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + tiers = await RewardTierRepository.get_by_program( + self.session, + program_id + ) + return [RewardTierResponse.model_validate(t) for t in tiers] diff --git a/PROJECTS/bug-bounty-platform/backend/app/py.typed b/PROJECTS/bug-bounty-platform/backend/app/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/Attachment.py b/PROJECTS/bug-bounty-platform/backend/app/report/Attachment.py new file mode 100644 index 00000000..600155ee --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/Attachment.py @@ -0,0 +1,64 @@ +""" +ⒸAngelaMos | 2025 +Attachment.py +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import BigInteger, ForeignKey, String +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + FILENAME_MAX_LENGTH, + MIME_TYPE_MAX_LENGTH, + STORAGE_PATH_MAX_LENGTH, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from report.Report import Report + + +class Attachment(Base, UUIDMixin, TimestampMixin): + """ + File attachment on a vulnerability report + """ + __tablename__ = "attachments" + + report_id: Mapped[UUID] = mapped_column( + ForeignKey("reports.id", + ondelete = "CASCADE"), + index = True, + ) + comment_id: Mapped[UUID | None] = mapped_column( + ForeignKey("comments.id", + ondelete = "SET NULL"), + default = None, + ) + + filename: Mapped[str] = mapped_column( + String(FILENAME_MAX_LENGTH), + ) + storage_path: Mapped[str] = mapped_column( + String(STORAGE_PATH_MAX_LENGTH), + ) + mime_type: Mapped[str] = mapped_column( + String(MIME_TYPE_MAX_LENGTH), + ) + size_bytes: Mapped[int] = mapped_column(BigInteger) + + report: Mapped[Report] = relationship( + back_populates = "attachments", + lazy = "raise", + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/Comment.py b/PROJECTS/bug-bounty-platform/backend/app/report/Comment.py new file mode 100644 index 00000000..98ba7c7e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/Comment.py @@ -0,0 +1,54 @@ +""" +ⒸAngelaMos | 2025 +Comment.py +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import ForeignKey, Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from report.Report import Report + from user.User import User + + +class Comment(Base, UUIDMixin, TimestampMixin): + """ + Comment on a vulnerability report + """ + __tablename__ = "comments" + + report_id: Mapped[UUID] = mapped_column( + ForeignKey("reports.id", + ondelete = "CASCADE"), + index = True, + ) + author_id: Mapped[UUID] = mapped_column( + ForeignKey("users.id", + ondelete = "CASCADE"), + index = True, + ) + + content: Mapped[str] = mapped_column(Text) + + is_internal: Mapped[bool] = mapped_column(default = False) + + report: Mapped[Report] = relationship( + back_populates = "comments", + lazy = "raise", + ) + author: Mapped[User] = relationship(lazy = "raise") diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/Report.py b/PROJECTS/bug-bounty-platform/backend/app/report/Report.py new file mode 100644 index 00000000..eba00c40 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/Report.py @@ -0,0 +1,185 @@ +""" +ⒸAngelaMos | 2025 +Report.py +""" + +from __future__ import annotations + +from datetime import UTC, datetime +from decimal import Decimal +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + CWE_ID_MAX_LENGTH, + REPORT_TITLE_MAX_LENGTH, + ReportStatus, + SafeEnum, + Severity, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from program.Program import Program + from report.Attachment import Attachment + from report.Comment import Comment + from user.User import User + + +class Report(Base, UUIDMixin, TimestampMixin): + """ + Vulnerability report submitted by a researcher + """ + __tablename__ = "reports" + + program_id: Mapped[UUID] = mapped_column( + ForeignKey("programs.id", + ondelete = "CASCADE"), + index = True, + ) + researcher_id: Mapped[UUID] = mapped_column( + ForeignKey("users.id", + ondelete = "CASCADE"), + index = True, + ) + + title: Mapped[str] = mapped_column( + String(REPORT_TITLE_MAX_LENGTH), + ) + description: Mapped[str] = mapped_column(Text) + steps_to_reproduce: Mapped[str | None] = mapped_column( + Text, + default = None, + ) + impact: Mapped[str | None] = mapped_column( + Text, + default = None, + ) + + severity_submitted: Mapped[Severity] = mapped_column( + SafeEnum(Severity), + default = Severity.MEDIUM, + ) + severity_final: Mapped[Severity | None] = mapped_column( + SafeEnum(Severity), + default = None, + ) + + status: Mapped[ReportStatus] = mapped_column( + SafeEnum(ReportStatus), + default = ReportStatus.NEW, + index = True, + ) + + cvss_score: Mapped[Decimal | None] = mapped_column( + Numeric(precision = 3, + scale = 1), + default = None, + ) + cwe_id: Mapped[str | None] = mapped_column( + String(CWE_ID_MAX_LENGTH), + default = None, + ) + + bounty_amount: Mapped[int | None] = mapped_column(default = None) + + duplicate_of_id: Mapped[UUID | None] = mapped_column( + ForeignKey("reports.id", + ondelete = "SET NULL"), + default = None, + ) + + triaged_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone = True), + default = None, + ) + resolved_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone = True), + default = None, + ) + disclosed_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone = True), + default = None, + ) + + program: Mapped[Program] = relationship( + back_populates = "reports", + lazy = "raise", + ) + researcher: Mapped[User] = relationship( + back_populates = "reports", + lazy = "raise", + ) + + comments: Mapped[list[Comment]] = relationship( + back_populates = "report", + cascade = "all, delete-orphan", + lazy = "raise", + ) + attachments: Mapped[list[Attachment]] = relationship( + back_populates = "report", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + duplicate_of: Mapped[Report | None] = relationship( + remote_side = "Report.id", + lazy = "raise", + ) + + def mark_triaging(self) -> None: + """ + Transition report to triaging status + """ + self.status = ReportStatus.TRIAGING + self.triaged_at = datetime.now(UTC) + + def mark_resolved(self) -> None: + """ + Transition report to resolved status + """ + self.status = ReportStatus.RESOLVED + self.resolved_at = datetime.now(UTC) + + def mark_disclosed(self) -> None: + """ + Transition report to disclosed status + """ + self.status = ReportStatus.DISCLOSED + self.disclosed_at = datetime.now(UTC) + + @property + def is_open(self) -> bool: + """ + Check if report is still open for action + """ + return self.status in ( + ReportStatus.NEW, + ReportStatus.TRIAGING, + ReportStatus.NEEDS_MORE_INFO, + ) + + @property + def is_closed(self) -> bool: + """ + Check if report has reached a terminal state + """ + return self.status in ( + ReportStatus.ACCEPTED, + ReportStatus.DUPLICATE, + ReportStatus.INFORMATIVE, + ReportStatus.NOT_APPLICABLE, + ReportStatus.RESOLVED, + ReportStatus.DISCLOSED, + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/report/__init__.py new file mode 100644 index 00000000..e863581c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/__init__.py @@ -0,0 +1,4 @@ +""" +ⒸAngelaMos | 2025 +__init__.py +""" diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/dependencies.py b/PROJECTS/bug-bounty-platform/backend/app/report/dependencies.py new file mode 100644 index 00000000..ab9fd9a6 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/dependencies.py @@ -0,0 +1,21 @@ +""" +ⒸAngelaMos | 2025 +dependencies.py +""" + +from typing import Annotated + +from fastapi import Depends + +from core.dependencies import DBSession +from .service import ReportService + + +def get_report_service(db: DBSession) -> ReportService: + """ + Dependency to inject ReportService instance + """ + return ReportService(db) + + +ReportServiceDep = Annotated[ReportService, Depends(get_report_service)] diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/repository.py b/PROJECTS/bug-bounty-platform/backend/app/report/repository.py new file mode 100644 index 00000000..d6e2d18a --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/repository.py @@ -0,0 +1,256 @@ +""" +ⒸAngelaMos | 2025 +repository.py +""" + +from typing import Any +from collections.abc import Sequence +from uuid import UUID + +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from config import ReportStatus +from core.base_repository import BaseRepository +from program.Program import Program +from .Report import Report +from .Comment import Comment +from .Attachment import Attachment + + +class ReportRepository(BaseRepository[Report]): + """ + Repository for Report model database operations + """ + model = Report + + @classmethod + async def get_by_id_with_details( + cls, + session: AsyncSession, + report_id: UUID, + ) -> Report | None: + """ + Get report by ID with comments and attachments + """ + result = await session.execute( + select(Report).where(Report.id == report_id).options( + selectinload(Report.comments), + selectinload(Report.attachments), + ) + ) + return result.scalars().first() + + @classmethod + async def get_by_researcher( + cls, + session: AsyncSession, + researcher_id: UUID, + skip: int = 0, + limit: int = 20, + ) -> Sequence[Report]: + """ + Get reports by researcher + """ + result = await session.execute( + select(Report).where(Report.researcher_id == researcher_id + ).order_by(Report.created_at.desc() + ).offset(skip).limit(limit) + ) + return result.scalars().all() + + @classmethod + async def count_by_researcher( + cls, + session: AsyncSession, + researcher_id: UUID, + ) -> int: + """ + Count reports by researcher + """ + result = await session.execute( + select(func.count()).select_from(Report).where( + Report.researcher_id == researcher_id + ) + ) + return result.scalar_one() + + @classmethod + async def get_by_program( + cls, + session: AsyncSession, + program_id: UUID, + skip: int = 0, + limit: int = 20, + status_filter: ReportStatus | None = None, + ) -> Sequence[Report]: + """ + Get reports for a program (inbox view) + """ + query = select(Report).where(Report.program_id == program_id) + + if status_filter: + query = query.where(Report.status == status_filter) + + result = await session.execute( + query.order_by(Report.created_at.desc() + ).offset(skip).limit(limit) + ) + return result.scalars().all() + + @classmethod + async def count_by_program( + cls, + session: AsyncSession, + program_id: UUID, + status_filter: ReportStatus | None = None, + ) -> int: + """ + Count reports for a program + """ + query = ( + select(func.count()).select_from(Report).where( + Report.program_id == program_id + ) + ) + + if status_filter: + query = query.where(Report.status == status_filter) + + result = await session.execute(query) + return result.scalar_one() + + @classmethod + async def get_inbox_for_company( + cls, + session: AsyncSession, + company_id: UUID, + skip: int = 0, + limit: int = 20, + ) -> Sequence[Report]: + """ + Get all reports across all programs owned by company + """ + result = await session.execute( + select(Report).join(Program, + Report.program_id == Program.id).where( + Program.company_id == company_id + ).order_by(Report.created_at.desc() + ).offset(skip).limit(limit) + ) + return result.scalars().all() + + @classmethod + async def count_inbox_for_company( + cls, + session: AsyncSession, + company_id: UUID, + ) -> int: + """ + Count all reports for company's programs + """ + result = await session.execute( + select(func.count()).select_from(Report).join( + Program, + Report.program_id == Program.id + ).where(Program.company_id == company_id) + ) + return result.scalar_one() + + @classmethod + async def get_researcher_stats( + cls, + session: AsyncSession, + researcher_id: UUID, + ) -> dict[str, + Any]: + """ + Get statistics for a researcher + """ + total_result = await session.execute( + select(func.count()).select_from(Report).where( + Report.researcher_id == researcher_id + ) + ) + total = total_result.scalar_one() + + accepted_result = await session.execute( + select(func.count()).select_from(Report).where( + Report.researcher_id == researcher_id, + Report.status.in_( + [ + ReportStatus.ACCEPTED, + ReportStatus.RESOLVED, + ReportStatus.DISCLOSED, + ] + ) + ) + ) + accepted = accepted_result.scalar_one() + + earned_result = await session.execute( + select(func.coalesce(func.sum(Report.bounty_amount), + 0)).where( + Report.researcher_id == researcher_id, + Report.bounty_amount.isnot(None), + ) + ) + earned = earned_result.scalar_one() + + return { + "total_reports": total, + "accepted_reports": accepted, + "total_earned": earned, + } + + +class CommentRepository(BaseRepository[Comment]): + """ + Repository for Comment model database operations + """ + model = Comment + + @classmethod + async def get_by_report( + cls, + session: AsyncSession, + report_id: UUID, + include_internal: bool = False, + ) -> Sequence[Comment]: + """ + Get comments for a report + """ + query = select(Comment).where(Comment.report_id == report_id) + + if not include_internal: + query = query.where(Comment.is_internal == False) + + result = await session.execute( + query.order_by(Comment.created_at.asc()) + ) + return result.scalars().all() + + +class AttachmentRepository(BaseRepository[Attachment]): + """ + Repository for Attachment model database operations + """ + model = Attachment + + @classmethod + async def get_by_report( + cls, + session: AsyncSession, + report_id: UUID, + ) -> Sequence[Attachment]: + """ + Get attachments for a report + """ + result = await session.execute( + select(Attachment).where(Attachment.report_id == report_id + ).order_by( + Attachment.created_at.asc() + ) + ) + return result.scalars().all() diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/routes.py b/PROJECTS/bug-bounty-platform/backend/app/report/routes.py new file mode 100644 index 00000000..1b08f996 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/routes.py @@ -0,0 +1,251 @@ +""" +ⒸAngelaMos | 2025 +routes.py +""" + +from uuid import UUID + +from fastapi import APIRouter, Query, status + +from config import ReportStatus +from core.dependencies import CurrentUser +from core.responses import ( + AUTH_401, + FORBIDDEN_403, + NOT_FOUND_404, +) +from .schemas import ( + CommentCreate, + CommentResponse, + ReportCreate, + ReportDetailResponse, + ReportListResponse, + ReportResponse, + ReportStatsResponse, + ReportTriageUpdate, + ReportUpdate, +) +from .dependencies import ReportServiceDep + + +router = APIRouter(prefix = "/reports", tags = ["reports"]) + + +@router.post( + "", + response_model = ReportResponse, + status_code = status.HTTP_201_CREATED, + responses = { + **AUTH_401, + **NOT_FOUND_404 + }, +) +async def submit_report( + report_service: ReportServiceDep, + current_user: CurrentUser, + report_data: ReportCreate, +) -> ReportResponse: + """ + Submit a new vulnerability report + """ + return await report_service.submit_report(current_user, report_data) + + +@router.get( + "", + response_model = ReportListResponse, + responses = {**AUTH_401}, +) +async def list_my_reports( + report_service: ReportServiceDep, + current_user: CurrentUser, + page: int = Query(default = 1, + ge = 1), + size: int = Query(default = 20, + ge = 1, + le = 100), +) -> ReportListResponse: + """ + List reports submitted by current user + """ + return await report_service.list_my_reports(current_user, page, size) + + +@router.get( + "/inbox", + response_model = ReportListResponse, + responses = {**AUTH_401}, +) +async def list_inbox( + report_service: ReportServiceDep, + current_user: CurrentUser, + page: int = Query(default = 1, + ge = 1), + size: int = Query(default = 20, + ge = 1, + le = 100), +) -> ReportListResponse: + """ + List all reports across user's programs (company inbox) + """ + return await report_service.list_inbox(current_user, page, size) + + +@router.get( + "/stats", + response_model = ReportStatsResponse, + responses = {**AUTH_401}, +) +async def get_my_stats( + report_service: ReportServiceDep, + current_user: CurrentUser, +) -> ReportStatsResponse: + """ + Get current user's report statistics + """ + return await report_service.get_my_stats(current_user) + + +@router.get( + "/program/{program_id}", + response_model = ReportListResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def list_program_reports( + report_service: ReportServiceDep, + current_user: CurrentUser, + program_id: UUID, + page: int = Query(default = 1, + ge = 1), + size: int = Query(default = 20, + ge = 1, + le = 100), + status_filter: ReportStatus | None = None, +) -> ReportListResponse: + """ + List reports for a specific program (program owner only) + """ + return await report_service.list_program_reports( + current_user, + program_id, + page, + size, + status_filter, + ) + + +@router.get( + "/{report_id}", + response_model = ReportDetailResponse, + responses = { + **AUTH_401, + **NOT_FOUND_404 + }, +) +async def get_report( + report_service: ReportServiceDep, + current_user: CurrentUser, + report_id: UUID, +) -> ReportDetailResponse: + """ + Get report by ID with full details + """ + return await report_service.get_report(current_user, report_id) + + +@router.patch( + "/{report_id}", + response_model = ReportResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def update_report( + report_service: ReportServiceDep, + current_user: CurrentUser, + report_id: UUID, + report_data: ReportUpdate, +) -> ReportResponse: + """ + Update report (researcher only, only if still open) + """ + return await report_service.update_report( + current_user, + report_id, + report_data + ) + + +@router.patch( + "/{report_id}/triage", + response_model = ReportResponse, + responses = { + **AUTH_401, + **FORBIDDEN_403, + **NOT_FOUND_404 + }, +) +async def triage_report( + report_service: ReportServiceDep, + current_user: CurrentUser, + report_id: UUID, + triage_data: ReportTriageUpdate, +) -> ReportResponse: + """ + Triage a report (program owner only) + """ + return await report_service.triage_report( + current_user, + report_id, + triage_data + ) + + +@router.get( + "/{report_id}/comments", + response_model = list[CommentResponse], + responses = { + **AUTH_401, + **NOT_FOUND_404 + }, +) +async def list_comments( + report_service: ReportServiceDep, + current_user: CurrentUser, + report_id: UUID, +) -> list[CommentResponse]: + """ + List comments for a report + """ + return await report_service.list_comments(current_user, report_id) + + +@router.post( + "/{report_id}/comments", + response_model = CommentResponse, + status_code = status.HTTP_201_CREATED, + responses = { + **AUTH_401, + **NOT_FOUND_404 + }, +) +async def add_comment( + report_service: ReportServiceDep, + current_user: CurrentUser, + report_id: UUID, + comment_data: CommentCreate, +) -> CommentResponse: + """ + Add comment to a report + """ + return await report_service.add_comment( + current_user, + report_id, + comment_data + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/schemas.py b/PROJECTS/bug-bounty-platform/backend/app/report/schemas.py new file mode 100644 index 00000000..6364fad2 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/schemas.py @@ -0,0 +1,141 @@ +""" +ⒸAngelaMos | 2025 +schemas.py +""" + +from uuid import UUID +from datetime import datetime +from decimal import Decimal + +from pydantic import Field + +from config import ( + CWE_ID_MAX_LENGTH, + REPORT_TITLE_MAX_LENGTH, + ReportStatus, + Severity, +) +from core.base_schema import ( + BaseSchema, + BaseResponseSchema, +) + + +class ReportCreate(BaseSchema): + """ + Schema for submitting a vulnerability report + """ + program_id: UUID + title: str = Field(max_length = REPORT_TITLE_MAX_LENGTH) + description: str + steps_to_reproduce: str | None = None + impact: str | None = None + severity_submitted: Severity = Severity.MEDIUM + + +class ReportUpdate(BaseSchema): + """ + Schema for researcher updating their report + """ + title: str | None = Field( + default = None, + max_length = REPORT_TITLE_MAX_LENGTH + ) + description: str | None = None + steps_to_reproduce: str | None = None + impact: str | None = None + severity_submitted: Severity | None = None + + +class ReportTriageUpdate(BaseSchema): + """ + Schema for company triaging a report + """ + status: ReportStatus | None = None + severity_final: Severity | None = None + cvss_score: Decimal | None = Field(default = None, ge = 0, le = 10) + cwe_id: str | None = Field( + default = None, + max_length = CWE_ID_MAX_LENGTH + ) + bounty_amount: int | None = Field(default = None, ge = 0) + duplicate_of_id: UUID | None = None + + +class ReportResponse(BaseResponseSchema): + """ + Schema for report API responses + """ + program_id: UUID + researcher_id: UUID + title: str + description: str + steps_to_reproduce: str | None + impact: str | None + severity_submitted: Severity + severity_final: Severity | None + status: ReportStatus + cvss_score: Decimal | None + cwe_id: str | None + bounty_amount: int | None + duplicate_of_id: UUID | None + triaged_at: datetime | None + resolved_at: datetime | None + disclosed_at: datetime | None + + +class ReportListResponse(BaseSchema): + """ + Schema for paginated report list + """ + items: list[ReportResponse] + total: int + page: int + size: int + + +class CommentCreate(BaseSchema): + """ + Schema for adding a comment to a report + """ + content: str + is_internal: bool = False + + +class CommentResponse(BaseResponseSchema): + """ + Schema for comment API responses + """ + report_id: UUID + author_id: UUID + content: str + is_internal: bool + + +class AttachmentResponse(BaseResponseSchema): + """ + Schema for attachment API responses + """ + report_id: UUID + comment_id: UUID | None + filename: str + mime_type: str + size_bytes: int + + +class ReportDetailResponse(ReportResponse): + """ + Schema for report detail with comments + """ + comments: list[CommentResponse] + attachments: list[AttachmentResponse] + + +class ReportStatsResponse(BaseSchema): + """ + Schema for researcher stats + """ + total_reports: int + accepted_reports: int + total_earned: int + reputation_score: int diff --git a/PROJECTS/bug-bounty-platform/backend/app/report/service.py b/PROJECTS/bug-bounty-platform/backend/app/report/service.py new file mode 100644 index 00000000..5c4b4ef2 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/report/service.py @@ -0,0 +1,381 @@ +""" +ⒸAngelaMos | 2025 +service.py +""" + +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession + +from config import ReportStatus +from core.exceptions import ( + CannotSubmitToOwnProgram, + NotProgramOwner, + NotReportOwner, + ProgramNotActive, + ProgramNotFound, + ReportNotFound, +) +from program.repository import ProgramRepository +from user.User import User +from .schemas import ( + AttachmentResponse, + CommentCreate, + CommentResponse, + ReportCreate, + ReportDetailResponse, + ReportListResponse, + ReportResponse, + ReportStatsResponse, + ReportTriageUpdate, + ReportUpdate, +) +from .repository import ( + CommentRepository, + ReportRepository, +) + + +class ReportService: + """ + Business logic for report operations + """ + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def submit_report( + self, + user: User, + report_data: ReportCreate, + ) -> ReportResponse: + """ + Submit a new vulnerability report + """ + program = await ProgramRepository.get_by_id( + self.session, + report_data.program_id + ) + if not program: + raise ProgramNotFound(str(report_data.program_id)) + + if not program.is_active: + raise ProgramNotActive() + + if program.company_id == user.id: + raise CannotSubmitToOwnProgram() + + report = await ReportRepository.create( + self.session, + program_id = report_data.program_id, + researcher_id = user.id, + title = report_data.title, + description = report_data.description, + steps_to_reproduce = report_data.steps_to_reproduce, + impact = report_data.impact, + severity_submitted = report_data.severity_submitted, + ) + return ReportResponse.model_validate(report) + + async def get_report( + self, + user: User, + report_id: UUID, + ) -> ReportDetailResponse: + """ + Get report by ID with details + """ + report = await ReportRepository.get_by_id_with_details( + self.session, + report_id + ) + if not report: + raise ReportNotFound(str(report_id)) + + program = await ProgramRepository.get_by_id( + self.session, + report.program_id + ) + + is_researcher = report.researcher_id == user.id + is_program_owner = program and program.company_id == user.id + + if not is_researcher and not is_program_owner: + raise ReportNotFound(str(report_id)) + + comments = report.comments + if is_researcher and not is_program_owner: + comments = [c for c in comments if not c.is_internal] + + return ReportDetailResponse( + id = report.id, + created_at = report.created_at, + updated_at = report.updated_at, + program_id = report.program_id, + researcher_id = report.researcher_id, + title = report.title, + description = report.description, + steps_to_reproduce = report.steps_to_reproduce, + impact = report.impact, + severity_submitted = report.severity_submitted, + severity_final = report.severity_final, + status = report.status, + cvss_score = report.cvss_score, + cwe_id = report.cwe_id, + bounty_amount = report.bounty_amount, + duplicate_of_id = report.duplicate_of_id, + triaged_at = report.triaged_at, + resolved_at = report.resolved_at, + disclosed_at = report.disclosed_at, + comments = [ + CommentResponse.model_validate(c) for c in comments + ], + attachments = [ + AttachmentResponse.model_validate(a) + for a in report.attachments + ], + ) + + async def list_my_reports( + self, + user: User, + page: int, + size: int, + ) -> ReportListResponse: + """ + List reports submitted by current user + """ + skip = (page - 1) * size + reports = await ReportRepository.get_by_researcher( + self.session, + researcher_id = user.id, + skip = skip, + limit = size, + ) + total = await ReportRepository.count_by_researcher( + self.session, + user.id + ) + return ReportListResponse( + items = [ReportResponse.model_validate(r) for r in reports], + total = total, + page = page, + size = size, + ) + + async def list_program_reports( + self, + user: User, + program_id: UUID, + page: int, + size: int, + status_filter: ReportStatus | None = None, + ) -> ReportListResponse: + """ + List reports for a program (program owner only) + """ + program = await ProgramRepository.get_by_id( + self.session, + program_id + ) + if not program: + raise ProgramNotFound(str(program_id)) + + if program.company_id != user.id: + raise NotProgramOwner() + + skip = (page - 1) * size + reports = await ReportRepository.get_by_program( + self.session, + program_id = program_id, + skip = skip, + limit = size, + status_filter = status_filter, + ) + total = await ReportRepository.count_by_program( + self.session, + program_id, + status_filter + ) + return ReportListResponse( + items = [ReportResponse.model_validate(r) for r in reports], + total = total, + page = page, + size = size, + ) + + async def list_inbox( + self, + user: User, + page: int, + size: int, + ) -> ReportListResponse: + """ + List all reports across user's programs + """ + skip = (page - 1) * size + reports = await ReportRepository.get_inbox_for_company( + self.session, + company_id = user.id, + skip = skip, + limit = size, + ) + total = await ReportRepository.count_inbox_for_company( + self.session, + user.id + ) + return ReportListResponse( + items = [ReportResponse.model_validate(r) for r in reports], + total = total, + page = page, + size = size, + ) + + async def update_report( + self, + user: User, + report_id: UUID, + report_data: ReportUpdate, + ) -> ReportResponse: + """ + Update report (researcher only, only if still open) + """ + report = await ReportRepository.get_by_id(self.session, report_id) + if not report: + raise ReportNotFound(str(report_id)) + + if report.researcher_id != user.id: + raise NotReportOwner() + + if not report.is_open: + raise ReportNotFound(str(report_id)) + + update_dict = report_data.model_dump(exclude_unset = True) + updated = await ReportRepository.update( + self.session, + report, + **update_dict, + ) + return ReportResponse.model_validate(updated) + + async def triage_report( + self, + user: User, + report_id: UUID, + triage_data: ReportTriageUpdate, + ) -> ReportResponse: + """ + Triage a report (program owner only) + """ + report = await ReportRepository.get_by_id(self.session, report_id) + if not report: + raise ReportNotFound(str(report_id)) + + program = await ProgramRepository.get_by_id( + self.session, + report.program_id + ) + if not program or program.company_id != user.id: + raise NotProgramOwner() + + update_dict = triage_data.model_dump(exclude_unset = True) + + if "status" in update_dict: + new_status = update_dict["status"] + if new_status == ReportStatus.TRIAGING and report.triaged_at is None: + report.mark_triaging() + elif new_status == ReportStatus.RESOLVED: + report.mark_resolved() + elif new_status == ReportStatus.DISCLOSED: + report.mark_disclosed() + + updated = await ReportRepository.update( + self.session, + report, + **update_dict, + ) + return ReportResponse.model_validate(updated) + + async def add_comment( + self, + user: User, + report_id: UUID, + comment_data: CommentCreate, + ) -> CommentResponse: + """ + Add comment to a report + """ + report = await ReportRepository.get_by_id(self.session, report_id) + if not report: + raise ReportNotFound(str(report_id)) + + program = await ProgramRepository.get_by_id( + self.session, + report.program_id + ) + + is_researcher = report.researcher_id == user.id + is_program_owner = program and program.company_id == user.id + + if not is_researcher and not is_program_owner: + raise ReportNotFound(str(report_id)) + + if comment_data.is_internal and not is_program_owner: + comment_data.is_internal = False + + comment = await CommentRepository.create( + self.session, + report_id = report_id, + author_id = user.id, + content = comment_data.content, + is_internal = comment_data.is_internal, + ) + return CommentResponse.model_validate(comment) + + async def list_comments( + self, + user: User, + report_id: UUID, + ) -> list[CommentResponse]: + """ + List comments for a report + """ + report = await ReportRepository.get_by_id(self.session, report_id) + if not report: + raise ReportNotFound(str(report_id)) + + program = await ProgramRepository.get_by_id( + self.session, + report.program_id + ) + + is_researcher = report.researcher_id == user.id + is_program_owner = bool(program and program.company_id == user.id) + + if not is_researcher and not is_program_owner: + raise ReportNotFound(str(report_id)) + + include_internal = is_program_owner + comments = await CommentRepository.get_by_report( + self.session, + report_id, + include_internal = include_internal, + ) + return [CommentResponse.model_validate(c) for c in comments] + + async def get_my_stats( + self, + user: User, + ) -> ReportStatsResponse: + """ + Get current user's report statistics + """ + stats = await ReportRepository.get_researcher_stats( + self.session, + user.id + ) + return ReportStatsResponse( + total_reports = stats["total_reports"], + accepted_reports = stats["accepted_reports"], + total_earned = stats["total_earned"], + reputation_score = user.reputation_score, + ) diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/User.py b/PROJECTS/bug-bounty-platform/backend/app/user/User.py new file mode 100644 index 00000000..50b82ff0 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/User.py @@ -0,0 +1,126 @@ +""" +ⒸAngelaMos | 2025 +User.py +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sqlalchemy import String, Text +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +from config import ( + COMPANY_NAME_MAX_LENGTH, + EMAIL_MAX_LENGTH, + FULL_NAME_MAX_LENGTH, + PASSWORD_HASH_MAX_LENGTH, + SafeEnum, + UserRole, + WEBSITE_MAX_LENGTH, +) +from core.Base import ( + Base, + TimestampMixin, + UUIDMixin, +) + +if TYPE_CHECKING: + from auth.RefreshToken import RefreshToken + from program.Program import Program + from report.Report import Report + + +class User(Base, UUIDMixin, TimestampMixin): + """ + User account model supporting both researchers and companies + """ + __tablename__ = "users" + + email: Mapped[str] = mapped_column( + String(EMAIL_MAX_LENGTH), + unique = True, + index = True, + ) + hashed_password: Mapped[str] = mapped_column( + String(PASSWORD_HASH_MAX_LENGTH) + ) + + full_name: Mapped[str | None] = mapped_column( + String(FULL_NAME_MAX_LENGTH), + default = None, + ) + + is_active: Mapped[bool] = mapped_column(default = True) + is_verified: Mapped[bool] = mapped_column(default = False) + + role: Mapped[UserRole] = mapped_column( + SafeEnum(UserRole, + unknown_value = UserRole.UNKNOWN), + default = UserRole.USER, + ) + + token_version: Mapped[int] = mapped_column(default = 0) + + company_name: Mapped[str | None] = mapped_column( + String(COMPANY_NAME_MAX_LENGTH), + default = None, + ) + bio: Mapped[str | None] = mapped_column( + Text, + default = None, + ) + website: Mapped[str | None] = mapped_column( + String(WEBSITE_MAX_LENGTH), + default = None, + ) + reputation_score: Mapped[int] = mapped_column(default = 0) + + refresh_tokens: Mapped[list[RefreshToken]] = relationship( + back_populates = "user", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + programs: Mapped[list[Program]] = relationship( + back_populates = "company", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + reports: Mapped[list[Report]] = relationship( + back_populates = "researcher", + cascade = "all, delete-orphan", + lazy = "raise", + ) + + def increment_token_version(self) -> None: + """ + Invalidate all existing tokens for this user + """ + self.token_version += 1 + + @property + def is_verified_company(self) -> bool: + """ + Check if user is a verified organization (optional upgrade) + """ + return self.role == UserRole.COMPANY + + @property + def can_submit_reports(self) -> bool: + """ + Any authenticated user can submit reports + """ + return self.role != UserRole.UNKNOWN + + @property + def can_create_program(self) -> bool: + """ + Any authenticated user can create a program + """ + return self.role != UserRole.UNKNOWN diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/__init__.py b/PROJECTS/bug-bounty-platform/backend/app/user/__init__.py new file mode 100644 index 00000000..10f02dc8 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/__init__.py @@ -0,0 +1,4 @@ +""" +ⒸAngelaMos | 2025 +User Domain +""" diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/dependencies.py b/PROJECTS/bug-bounty-platform/backend/app/user/dependencies.py new file mode 100644 index 00000000..631bd79f --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/dependencies.py @@ -0,0 +1,21 @@ +""" +ⒸAngelaMos | 2025 +dependencies.py +""" + +from typing import Annotated + +from fastapi import Depends + +from core.dependencies import DBSession +from .service import UserService + + +def get_user_service(db: DBSession) -> UserService: + """ + Dependency to inject UserService instance + """ + return UserService(db) + + +UserServiceDep = Annotated[UserService, Depends(get_user_service)] diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/py.typed b/PROJECTS/bug-bounty-platform/backend/app/user/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/repository.py b/PROJECTS/bug-bounty-platform/backend/app/user/repository.py new file mode 100644 index 00000000..1c0eba39 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/repository.py @@ -0,0 +1,111 @@ +""" +ⒸAngelaMos | 2025 +repository.py +""" +from uuid import UUID + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from config import UserRole +from .User import User +from core.base_repository import BaseRepository + + +class UserRepository(BaseRepository[User]): + """ + Repository for User model database operations + """ + model = User + + @classmethod + async def get_by_email( + cls, + session: AsyncSession, + email: str, + ) -> User | None: + """ + Get user by email address + """ + result = await session.execute( + select(User).where(User.email == email) + ) + return result.scalars().first() + + @classmethod + async def get_by_id( + cls, + session: AsyncSession, + id: UUID, + ) -> User | None: + """ + Get user by ID + """ + return await session.get(User, id) + + @classmethod + async def email_exists( + cls, + session: AsyncSession, + email: str, + ) -> bool: + """ + Check if email is already registered + """ + result = await session.execute( + select(User.id).where(User.email == email) + ) + return result.scalars().first() is not None + + @classmethod + async def create_user( + cls, + session: AsyncSession, + email: str, + hashed_password: str, + full_name: str | None = None, + role: UserRole = UserRole.USER, + ) -> User: + """ + Create a new user + """ + user = User( + email = email, + hashed_password = hashed_password, + full_name = full_name, + role = role, + ) + session.add(user) + await session.flush() + await session.refresh(user) + return user + + @classmethod + async def update_password( + cls, + session: AsyncSession, + user: User, + hashed_password: str, + ) -> User: + """ + Update user password and increment token version + """ + user.hashed_password = hashed_password + user.increment_token_version() + await session.flush() + await session.refresh(user) + return user + + @classmethod + async def increment_token_version( + cls, + session: AsyncSession, + user: User, + ) -> User: + """ + Invalidate all user tokens + """ + user.increment_token_version() + await session.flush() + await session.refresh(user) + return user diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/routes.py b/PROJECTS/bug-bounty-platform/backend/app/user/routes.py new file mode 100644 index 00000000..4616804b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/routes.py @@ -0,0 +1,78 @@ +""" +ⒸAngelaMos | 2025 +routes.py +""" + +from uuid import UUID + +from fastapi import ( + APIRouter, + status, +) + +from core.dependencies import CurrentUser +from core.responses import ( + AUTH_401, + CONFLICT_409, + NOT_FOUND_404, +) +from .schemas import ( + UserCreate, + UserResponse, + UserUpdate, +) +from .dependencies import UserServiceDep + + +router = APIRouter(prefix = "/users", tags = ["users"]) + + +@router.post( + "", + response_model = UserResponse, + status_code = status.HTTP_201_CREATED, + responses = {**CONFLICT_409}, +) +async def create_user( + user_service: UserServiceDep, + user_data: UserCreate, +) -> UserResponse: + """ + Register a new user + """ + return await user_service.create_user(user_data) + + +@router.get( + "/{user_id}", + response_model = UserResponse, + responses = { + **AUTH_401, + **NOT_FOUND_404 + }, +) +async def get_user( + user_service: UserServiceDep, + user_id: UUID, + _: CurrentUser, +) -> UserResponse: + """ + Get user by ID + """ + return await user_service.get_user_by_id(user_id) + + +@router.patch( + "/me", + response_model = UserResponse, + responses = {**AUTH_401}, +) +async def update_current_user( + user_service: UserServiceDep, + current_user: CurrentUser, + user_data: UserUpdate, +) -> UserResponse: + """ + Update current user profile + """ + return await user_service.update_user(current_user, user_data) diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/schemas.py b/PROJECTS/bug-bounty-platform/backend/app/user/schemas.py new file mode 100644 index 00000000..be4c9e32 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/schemas.py @@ -0,0 +1,109 @@ +""" +ⒸAngelaMos | 2025 +schemas.py +""" + +from pydantic import ( + Field, + EmailStr, + field_validator, +) + +from config import ( + UserRole, + FULL_NAME_MAX_LENGTH, + PASSWORD_MAX_LENGTH, + PASSWORD_MIN_LENGTH, +) +from core.base_schema import ( + BaseSchema, + BaseResponseSchema, +) + + +class UserCreate(BaseSchema): + """ + Schema for user registration + """ + email: EmailStr + password: str = Field( + min_length = PASSWORD_MIN_LENGTH, + max_length = PASSWORD_MAX_LENGTH + ) + full_name: str | None = Field( + default = None, + max_length = FULL_NAME_MAX_LENGTH + ) + + @field_validator("password") + @classmethod + def validate_password_strength(cls, v: str) -> str: + """ + Ensure password has minimum complexity + """ + if not any(c.isupper() for c in v): + raise ValueError( + "Password must contain at least one uppercase letter" + ) + if not any(c.isdigit() for c in v): + raise ValueError("Password must contain at least one digit") + return v + + +class UserUpdate(BaseSchema): + """ + Schema for updating user profile + """ + full_name: str | None = Field( + default = None, + max_length = FULL_NAME_MAX_LENGTH + ) + + +class UserUpdateAdmin(UserUpdate): + """ + Schema for admin updating user + """ + email: EmailStr | None = None + is_active: bool | None = None + is_verified: bool | None = None + role: UserRole | None = None + + +class AdminUserCreate(BaseSchema): + """ + Schema for admin creating a user + """ + email: EmailStr + password: str = Field( + min_length = PASSWORD_MIN_LENGTH, + max_length = PASSWORD_MAX_LENGTH + ) + full_name: str | None = Field( + default = None, + max_length = FULL_NAME_MAX_LENGTH + ) + role: UserRole = UserRole.USER + is_active: bool = True + is_verified: bool = False + + +class UserResponse(BaseResponseSchema): + """ + Schema for user API responses + """ + email: EmailStr + full_name: str | None + is_active: bool + is_verified: bool + role: UserRole + + +class UserListResponse(BaseSchema): + """ + Schema for paginated user list + """ + items: list[UserResponse] + total: int + page: int + size: int diff --git a/PROJECTS/bug-bounty-platform/backend/app/user/service.py b/PROJECTS/bug-bounty-platform/backend/app/user/service.py new file mode 100644 index 00000000..3a7d085c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/app/user/service.py @@ -0,0 +1,221 @@ +""" +ⒸAngelaMos | 2025 +service.py +""" + +from uuid import UUID +from sqlalchemy.ext.asyncio import ( + AsyncSession, +) + +from config import settings, UserRole +from core.exceptions import ( + EmailAlreadyExists, + InvalidCredentials, + UserNotFound, +) +from core.security import ( + hash_password, + verify_password, +) +from .schemas import ( + AdminUserCreate, + UserCreate, + UserListResponse, + UserResponse, + UserUpdate, + UserUpdateAdmin, +) +from .User import User +from .repository import UserRepository + + +class UserService: + """ + Business logic for user operations + """ + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def create_user( + self, + user_data: UserCreate, + ) -> UserResponse: + """ + Register a new user + """ + if await UserRepository.email_exists(self.session, + user_data.email): + raise EmailAlreadyExists(user_data.email) + + role = UserRole.USER + if settings.ADMIN_EMAIL and user_data.email.lower( + ) == settings.ADMIN_EMAIL.lower(): + role = UserRole.ADMIN + + hashed = await hash_password(user_data.password) + user = await UserRepository.create_user( + self.session, + email = user_data.email, + hashed_password = hashed, + full_name = user_data.full_name, + role = role, + ) + return UserResponse.model_validate(user) + + async def get_user_by_id( + self, + user_id: UUID, + ) -> UserResponse: + """ + Get user by ID + """ + user = await UserRepository.get_by_id(self.session, user_id) + if not user: + raise UserNotFound(str(user_id)) + return UserResponse.model_validate(user) + + async def get_user_model_by_id( + self, + user_id: UUID, + ) -> User: + """ + Get user model by ID (for internal use) + """ + user = await UserRepository.get_by_id(self.session, user_id) + if not user: + raise UserNotFound(str(user_id)) + return user + + async def update_user( + self, + user: User, + user_data: UserUpdate, + ) -> UserResponse: + """ + Update user profile + """ + update_dict = user_data.model_dump(exclude_unset = True) + updated_user = await UserRepository.update( + self.session, + user, + **update_dict + ) + return UserResponse.model_validate(updated_user) + + async def change_password( + self, + user: User, + current_password: str, + new_password: str, + ) -> None: + """ + Change user password + """ + is_valid, _ = await verify_password(current_password, user.hashed_password) + if not is_valid: + raise InvalidCredentials() + + hashed = await hash_password(new_password) + await UserRepository.update_password(self.session, user, hashed) + + async def deactivate_user( + self, + user: User, + ) -> UserResponse: + """ + Deactivate user account + """ + updated = await UserRepository.update( + self.session, + user, + is_active = False + ) + return UserResponse.model_validate(updated) + + async def list_users( + self, + page: int, + size: int, + ) -> UserListResponse: + """ + List users with pagination + """ + skip = (page - 1) * size + users = await UserRepository.get_multi( + self.session, + skip = skip, + limit = size + ) + total = await UserRepository.count(self.session) + return UserListResponse( + items = [UserResponse.model_validate(u) for u in users], + total = total, + page = page, + size = size, + ) + + async def admin_create_user( + self, + user_data: AdminUserCreate, + ) -> UserResponse: + """ + Admin creates a new user + """ + if await UserRepository.email_exists(self.session, + user_data.email): + raise EmailAlreadyExists(user_data.email) + + hashed = await hash_password(user_data.password) + user = await UserRepository.create( + self.session, + email = user_data.email, + hashed_password = hashed, + full_name = user_data.full_name, + role = user_data.role, + is_active = user_data.is_active, + is_verified = user_data.is_verified, + ) + return UserResponse.model_validate(user) + + async def admin_update_user( + self, + user_id: UUID, + user_data: UserUpdateAdmin, + ) -> UserResponse: + """ + Admin updates a user + """ + user = await UserRepository.get_by_id(self.session, user_id) + if not user: + raise UserNotFound(str(user_id)) + + update_dict = user_data.model_dump(exclude_unset = True) + + if "email" in update_dict: + existing = await UserRepository.get_by_email( + self.session, + update_dict["email"] + ) + if existing and existing.id != user_id: + raise EmailAlreadyExists(update_dict["email"]) + + updated_user = await UserRepository.update( + self.session, + user, + **update_dict + ) + return UserResponse.model_validate(updated_user) + + async def admin_delete_user( + self, + user_id: UUID, + ) -> None: + """ + Admin deletes a user (hard delete) + """ + user = await UserRepository.get_by_id(self.session, user_id) + if not user: + raise UserNotFound(str(user_id)) + + await UserRepository.delete(self.session, user) diff --git a/PROJECTS/bug-bounty-platform/backend/conftest.py b/PROJECTS/bug-bounty-platform/backend/conftest.py new file mode 100644 index 00000000..9b95ea0a --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/conftest.py @@ -0,0 +1,291 @@ +""" +©AngelaMos | 2025 +conftest.py + +Test configuration, fixtures, and factories +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "app")) + +import hashlib +import secrets +from datetime import ( + UTC, + datetime, + timedelta, +) +from uuid import uuid4 +from collections.abc import AsyncIterator + +import pytest +from httpx import ( + AsyncClient, + ASGITransport, +) +import pytest_asyncio +from sqlalchemy.ext.asyncio import ( + AsyncSession, + create_async_engine, +) +from sqlalchemy.pool import StaticPool + +from core.security import ( + hash_password, + create_access_token, +) +from config import UserRole +from core.database import get_db_session + +from core.Base import Base +from user.User import User +from auth.RefreshToken import RefreshToken + + +@pytest_asyncio.fixture(scope = "session", loop_scope = "session") +async def test_engine(): + """ + Session scoped async engine with in memory SQLite + StaticPool keeps single connection so DB persists + """ + engine = create_async_engine( + "sqlite+aiosqlite:///:memory:", + poolclass = StaticPool, + connect_args = {"check_same_thread": False}, + echo = False, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + yield engine + await engine.dispose() + + +@pytest.fixture +async def db_session(test_engine) -> AsyncIterator[AsyncSession]: + """ + Per test session with transaction rollback for isolation + App commits become savepoints that rollback with test + """ + async with test_engine.connect() as conn: + await conn.begin() + + session = AsyncSession( + bind = conn, + expire_on_commit = False, + join_transaction_mode = "create_savepoint", + ) + + yield session + + await session.close() + await conn.rollback() + + +@pytest.fixture +async def client(db_session: AsyncSession) -> AsyncIterator[AsyncClient]: + """ + Async HTTP client with DB session override + """ + from factory import create_app + + app = create_app() + + async def override_get_db(): + yield db_session + + app.dependency_overrides[get_db_session] = override_get_db + + async with AsyncClient( + transport = ASGITransport(app = app), + base_url = "http://test", + ) as ac: + yield ac + + app.dependency_overrides.clear() + + +@pytest.fixture +def auth_headers(access_token: str) -> dict[str, str]: + """ + Authorization headers for authenticated requests + """ + return {"Authorization": f"Bearer {access_token}"} + + +@pytest.fixture +def admin_auth_headers(admin_access_token: str) -> dict[str, str]: + """ + Authorization headers for admin requests + """ + return {"Authorization": f"Bearer {admin_access_token}"} + + +class UserFactory: + """ + Factory for creating test users + """ + _counter = 0 + + @classmethod + async def create( + cls, + session: AsyncSession, + *, + email: str | None = None, + password: str = "TestPass123", + full_name: str | None = None, + role: UserRole = UserRole.USER, + is_active: bool = True, + is_verified: bool = True, + ) -> User: + cls._counter += 1 + + user = User( + email = email or f"user{cls._counter}@test.com", + hashed_password = await hash_password(password), + full_name = full_name or f"Test User {cls._counter}", + role = role, + is_active = is_active, + is_verified = is_verified, + ) + session.add(user) + await session.flush() + await session.refresh(user) + return user + + @classmethod + def reset(cls) -> None: + cls._counter = 0 + + +class RefreshTokenFactory: + """ + Factory for creating test refresh tokens + """ + @classmethod + async def create( + cls, + session: AsyncSession, + user: User, + *, + is_revoked: bool = False, + expires_delta: timedelta = timedelta(days = 7), + ) -> tuple[RefreshToken, + str]: + raw_token = secrets.token_urlsafe(32) + token_hash = hashlib.sha256(raw_token.encode()).hexdigest() + + token = RefreshToken( + user_id = user.id, + token_hash = token_hash, + family_id = uuid4(), + expires_at = datetime.now(UTC) + expires_delta, + is_revoked = is_revoked, + ) + session.add(token) + await session.flush() + await session.refresh(token) + return token, raw_token + + +@pytest.fixture +async def test_user(db_session: AsyncSession) -> User: + """ + Standard test user + """ + return await UserFactory.create(db_session) + + +@pytest.fixture +async def admin_user(db_session: AsyncSession) -> User: + """ + Admin test user + """ + return await UserFactory.create( + db_session, + email = "admin@test.com", + role = UserRole.ADMIN, + ) + + +@pytest.fixture +async def inactive_user(db_session: AsyncSession) -> User: + """ + Inactive test user + """ + return await UserFactory.create( + db_session, + email = "inactive@test.com", + is_active = False, + ) + + +@pytest.fixture +def access_token(test_user: User) -> str: + """ + Valid access token for test_user + """ + return create_access_token(test_user.id, test_user.token_version) + + +@pytest.fixture +def admin_access_token(admin_user: User) -> str: + """ + Valid access token for admin_user + """ + return create_access_token(admin_user.id, admin_user.token_version) + + +@pytest.fixture +async def refresh_token_pair( + db_session: AsyncSession, + test_user: User, +) -> tuple[RefreshToken, + str]: + """ + Refresh token DB record and raw token string + """ + return await RefreshTokenFactory.create(db_session, test_user) + + +@pytest.fixture +async def expired_refresh_token_pair( + db_session: AsyncSession, + test_user: User, +) -> tuple[RefreshToken, + str]: + """ + Expired refresh token for testing + """ + return await RefreshTokenFactory.create( + db_session, + test_user, + expires_delta = timedelta(days = -1), + ) + + +@pytest.fixture +async def revoked_refresh_token_pair( + db_session: AsyncSession, + test_user: User, +) -> tuple[RefreshToken, + str]: + """ + Revoked refresh token for testing + """ + return await RefreshTokenFactory.create( + db_session, + test_user, + is_revoked = True, + ) + + +@pytest.fixture(autouse = True) +def reset_factories(): + """ + Reset factory counters between tests + """ + yield + UserFactory.reset() diff --git a/PROJECTS/bug-bounty-platform/backend/pyproject.toml b/PROJECTS/bug-bounty-platform/backend/pyproject.toml new file mode 100644 index 00000000..eed0abf8 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/pyproject.toml @@ -0,0 +1,287 @@ +[project] +name = "bug-bounty-platform" +version = "1.0.0" +description = "Full stack bug bounty platform" +requires-python = ">=3.12" + +dependencies = [ + "fastapi[standard]>=0.123.0,<1.0.0", + "pydantic>=2.12.5,<3.0.0", + "pydantic-settings>=2.12.0,<3.0.0", + "psycopg2-binary>=2.9.11", + "sqlalchemy>=2.0.44,<3.0.0", + "alembic>=1.17.0,<2.0.0", + "asyncpg>=0.31.0,<1.0.0", + "python-multipart>=0.0.20", + "pyjwt>=2.10.0", + "pwdlib[argon2]>=0.3.0", + "uuid6>=2025.0.1", + "slowapi>=0.1.9", + "redis>=7.1.0", + "structlog>=24.4.0", + "gunicorn>=23.0.0", + "uvicorn[standard]>=0.38.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", + "pytest-cov>=6.0.0", + "httpx>=0.28.1", + "aiosqlite>=0.21.0", + "asgi-lifespan>=2.1.0", + "mypy>=1.19.0", + "types-redis>=4.6.0", + "ruff>=0.14.8", + "pylint>=4.0.4", + "pylint-pydantic>=0.4.1", + "pylint-per-file-ignores>=3.2.0", + "ty>=0.0.1a32", + "pre-commit>=4.2.0", +] + +[project.urls] +Homepage = "https://bugbountyplatform.sh" +Repository = "https://github.com/CarterPerez-dev/Cybersecurity-Projects" +Issues = "https://github.com/CarterPerez-dev/Cybersecurity-Projects/issues" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["app"] + +[tool.ruff] +target-version = "py312" +line-length = 88 +src = ["app"] +exclude = ["alembic"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify + "PTH", # flake8-use-pathlib + "RUF", # ruff-specific + "ASYNC", # flake8-async + "S", # flake8-bandit (security) + "N", # pep8-naming +] +ignore = [ + "E501", # line too long (formatter handles this) + "B008", # function call in default argument (FastAPI Depends) + "S101", # assert usage (needed for tests) + "S104", # 0.0.0.0 binding (intentional for Docker) + "S105", # "bearer" token_type is not a password + "ARG001", # unused function argument (common in FastAPI deps) + "E712", # == False is REQUIRED for SQLAlchemy WHERE clauses + "N999", # PascalCase module names (intentional: Base.py, User.py) + "N818", # exception naming convention (style preference) + "UP046", # Generic[T] syntax (keep for compatibility) + "RUF005", # list concatenation style (preference) +] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["S101", "ARG001"] +"conftest.py" = ["S107"] +"app/core/rate_limit.py" = ["S110"] +"app/config.py" = ["F401"] +"app/**/schemas.py" = ["RUF012"] +"app/core/error_schemas.py" = ["RUF012"] + +[tool.mypy] +python_version = "3.12" +strict = true +warn_return_any = true +warn_unused_ignores = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +plugins = ["pydantic.mypy"] +exclude = ["alembic", ".venv", "venv"] + +[[tool.mypy.overrides]] +module = ["tests.*", "conftest"] +ignore_errors = true + +[[tool.mypy.overrides]] +module = ["core.logging"] +disable_error_code = ["no-any-return"] + +[[tool.mypy.overrides]] +module = [ + "uuid6", + "structlog", + "structlog.*", + "pwdlib", + "slowapi", + "slowapi.*", +] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["pydantic_settings.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["config"] +implicit_reexport = true + +[[tool.mypy.overrides]] +module = ["core.enums", "core.security"] +disable_error_code = ["return-value", "no-any-return"] + +[[tool.mypy.overrides]] +module = ["user.repository", "auth.repository", "core.base_repository"] +disable_error_code = ["return-value", "no-any-return", "attr-defined"] + +[[tool.mypy.overrides]] +module = ["user.service", "auth.service"] +disable_error_code = ["no-any-return"] + +[[tool.mypy.overrides]] +module = ["factory"] +disable_error_code = ["arg-type"] + +[[tool.mypy.overrides]] +module = ["auth.routes"] +disable_error_code = ["misc"] + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + + +[tool.pylint.main] +py-version = "3.11" +jobs = 4 +load-plugins = [ + "pylint_pydantic", + "pylint_per_file_ignores", +] +persistent = true +ignore = [ + "alembic", + "venv", + ".venv", + "__pycache__", + "build", + "dist", + ".git", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", +] +ignore-paths = [ + "^alembic/.*", + "^venv/.*", + "^.venv/.*", + "^build/.*", + "^dist/.*", +] + +[tool.pylint.messages_control] +disable = [ + "C0103", # invalid-name + "C0116", # missing-function-docstring (we use minimal docs) + "C0121", # singleton-comparison (== False required for SQLAlchemy) + "C0301", # line-too-long + "C0302", # too-many-lines + "C0303", # trailing-whitespace + "C0304", # final-newline-missing + "C0305", # trailing-newlines + "C0411", # wrong-import-order + "C0412", # ungrouped-imports (style preference) + "E0401", # import-error (uuid6/structlog/pwdlib not found by pylint) + "E0611", # no-name-in-module (false positive for config re-exports) + "E1102", # not-callable (false positive for SQLAlchemy func.now/count) + "E1136", # unsubscriptable-object (false positive for generics) + "R0801", # similar-lines + "R0901", # too-many-ancestors (SQLAlchemy inheritance) + "R0903", # too-few-public-methods + "R0917", # too-many-positional-arguments (FastAPI patterns) + "W0611", # unused-import (handled by ruff, config.py re-exports) + "W0612", # unused-variable (handled by ruff) + "W0613", # unused-argument (handled by ruff) + "W0621", # redefined-outer-name (FastAPI route/param naming) + "W0622", # redefined-builtin + "W0718", # broad-exception-caught (intentional in health/rate-limit) +] + +[tool.pylint-per-file-ignores] +"alembic/env.py" = "no-member" +"conftest.py" = "import-outside-toplevel" +"app/__main__.py" = "pointless-string-statement" + +[tool.pylint.format] +max-line-length = 95 + +[tool.pylint.design] +max-args = 12 +max-attributes = 10 +max-branches = 15 +max-locals = 20 +max-statements = 55 + + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +testpaths = ["tests"] +addopts = "-ra -q" +filterwarnings = [ + "ignore::DeprecationWarning", +] + +[tool.coverage.run] +branch = true +source = ["src"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", +] + + +[tool.ty.src] +include = ["app", "tests"] +exclude = ["alembic/versions/**", ".venv/**"] +respect-ignore-files = true + +[tool.ty.environment] +python-version = "3.12" +root = ["./app"] +python = "./.venv" + +[tool.ty.rules] +possibly-missing-attribute = "error" +possibly-missing-import = "error" +unused-ignore-comment = "warn" +redundant-cast = "warn" +undefined-reveal = "warn" + +[[tool.ty.overrides]] +include = ["tests/**"] +[tool.ty.overrides.rules] +unresolved-reference = "warn" +invalid-argument-type = "warn" + +[[tool.ty.overrides]] +include = ["app/repositories/**", "app/services/**"] +[tool.ty.overrides.rules] +unresolved-attribute = "warn" + +[tool.ty.terminal] +error-on-warning = false +output-format = "full" diff --git a/PROJECTS/bug-bounty-platform/backend/tests/__init__.py b/PROJECTS/bug-bounty-platform/backend/tests/__init__.py new file mode 100644 index 00000000..7fc7366b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/__init__.py @@ -0,0 +1,4 @@ +""" +AngelaMos | 2025 +__init__.py +""" diff --git a/PROJECTS/bug-bounty-platform/backend/tests/integration/__init__.py b/PROJECTS/bug-bounty-platform/backend/tests/integration/__init__.py new file mode 100644 index 00000000..7fc7366b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/integration/__init__.py @@ -0,0 +1,4 @@ +""" +AngelaMos | 2025 +__init__.py +""" diff --git a/PROJECTS/bug-bounty-platform/backend/tests/integration/test_admin.py b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_admin.py new file mode 100644 index 00000000..6c97f62d --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_admin.py @@ -0,0 +1,256 @@ +""" +©AngelaMos | 2025 +test_admin.py +""" + +import pytest +from httpx import AsyncClient + +from user.User import User + + +URL_ADMIN_USERS = "/v1/admin/users" + + +def url_admin_user_by_id(user_id: str) -> str: + return f"{URL_ADMIN_USERS}/{user_id}" + + +@pytest.mark.asyncio +async def test_admin_create_user( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], +): + """ + Admin can create a new user + """ + response = await client.post( + URL_ADMIN_USERS, + headers = admin_auth_headers, + json = { + "email": "adminmade@test.com", + "password": "ValidPass123", + "full_name": "Admin Created User", + }, + ) + + assert response.status_code == 201 + data = response.json() + assert data["email"] == "adminmade@test.com" + assert data["full_name"] == "Admin Created User" + assert "id" in data + assert "hashed_password" not in data + + +@pytest.mark.asyncio +async def test_admin_create_user_non_admin_forbidden( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, str], +): + """ + Non admin cannot create user via admin endpoint + """ + response = await client.post( + URL_ADMIN_USERS, + headers = auth_headers, + json = { + "email": "shouldfail@test.com", + "password": "ValidPass123", + }, + ) + + assert response.status_code == 403 + + +@pytest.mark.asyncio +async def test_admin_create_user_duplicate_email( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], + test_user: User, +): + """ + Admin create with duplicate email returns 409 + """ + response = await client.post( + URL_ADMIN_USERS, + headers = admin_auth_headers, + json = { + "email": test_user.email, + "password": "ValidPass123", + }, + ) + + assert response.status_code == 409 + + +@pytest.mark.asyncio +async def test_admin_get_user_by_id( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], + test_user: User, +): + """ + Admin can get any user by ID + """ + response = await client.get( + url_admin_user_by_id(str(test_user.id)), + headers = admin_auth_headers, + ) + + assert response.status_code == 200 + data = response.json() + assert data["id"] == str(test_user.id) + assert data["email"] == test_user.email + + +@pytest.mark.asyncio +async def test_admin_get_user_not_found( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], +): + """ + Admin get non existent user returns 404 + """ + fake_id = "00000000-0000-0000-0000-000000000000" + response = await client.get( + url_admin_user_by_id(fake_id), + headers = admin_auth_headers, + ) + + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_admin_update_user( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], + test_user: User, +): + """ + Admin can update any user + """ + response = await client.patch( + url_admin_user_by_id(str(test_user.id)), + headers = admin_auth_headers, + json = {"full_name": "Admin Updated Name"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["full_name"] == "Admin Updated Name" + assert data["id"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_admin_update_user_not_found( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], +): + """ + Admin update non existent user returns 404 + """ + fake_id = "00000000-0000-0000-0000-000000000000" + response = await client.patch( + url_admin_user_by_id(fake_id), + headers = admin_auth_headers, + json = {"full_name": "Should Fail"}, + ) + + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_admin_update_user_non_admin_forbidden( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, str], +): + """ + Non admin cannot update via admin endpoint + """ + response = await client.patch( + url_admin_user_by_id(str(test_user.id)), + headers = auth_headers, + json = {"full_name": "Should Fail"}, + ) + + assert response.status_code == 403 + + +@pytest.mark.asyncio +async def test_admin_delete_user( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], + db_session, +): + """ + Admin can delete a user + """ + from user.User import User as UserModel + from core.security import hash_password + + user_to_delete = UserModel( + email = "deleteme@test.com", + hashed_password = await hash_password("TestPass123"), + full_name = "Delete Me", + ) + db_session.add(user_to_delete) + await db_session.flush() + await db_session.refresh(user_to_delete) + user_id = str(user_to_delete.id) + + response = await client.delete( + url_admin_user_by_id(user_id), + headers = admin_auth_headers, + ) + + assert response.status_code == 204 + + get_response = await client.get( + url_admin_user_by_id(user_id), + headers = admin_auth_headers, + ) + assert get_response.status_code == 404 + + +@pytest.mark.asyncio +async def test_admin_delete_user_not_found( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, str], +): + """ + Admin delete non existent user returns 404 + """ + fake_id = "00000000-0000-0000-0000-000000000000" + response = await client.delete( + url_admin_user_by_id(fake_id), + headers = admin_auth_headers, + ) + + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_admin_delete_user_non_admin_forbidden( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, str], +): + """ + Non admin cannot delete users + """ + response = await client.delete( + url_admin_user_by_id(str(test_user.id)), + headers = auth_headers, + ) + + assert response.status_code == 403 diff --git a/PROJECTS/bug-bounty-platform/backend/tests/integration/test_auth.py b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_auth.py new file mode 100644 index 00000000..689b4970 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_auth.py @@ -0,0 +1,288 @@ +""" +©AngelaMos | 2025 +test_auth.py +""" + +import pytest +from httpx import AsyncClient + +from user.User import User +from auth.RefreshToken import RefreshToken + + +URL_LOGIN = "/v1/auth/login" +URL_REFRESH = "/v1/auth/refresh" +URL_LOGOUT = "/v1/auth/logout" +URL_LOGOUT_ALL = "/v1/auth/logout-all" +URL_ME = "/v1/auth/me" +URL_CHANGE_PASSWORD = "/v1/auth/change-password" + + +@pytest.mark.asyncio +async def test_login_success(client: AsyncClient, test_user: User): + """ + Valid credentials return access token and set refresh cookie + """ + response = await client.post( + URL_LOGIN, + data = { + "username": test_user.email, + "password": "TestPass123", + }, + ) + + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + assert "user" in data + assert data["user"]["email"] == test_user.email + assert "refresh_token" in response.cookies + + +@pytest.mark.asyncio +async def test_login_invalid_password( + client: AsyncClient, + test_user: User +): + """ + Wrong password returns 401 + """ + response = await client.post( + URL_LOGIN, + data = { + "username": test_user.email, + "password": "WrongPassword123", + }, + ) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_login_invalid_email(client: AsyncClient): + """ + Non-existent email returns 401 + """ + response = await client.post( + URL_LOGIN, + data = { + "username": "nonexistent@test.com", + "password": "TestPass123", + }, + ) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_login_inactive_user( + client: AsyncClient, + inactive_user: User +): + """ + Inactive user cannot login + """ + response = await client.post( + URL_LOGIN, + data = { + "username": inactive_user.email, + "password": "TestPass123", + }, + ) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_refresh_token_success( + client: AsyncClient, + refresh_token_pair: tuple[RefreshToken, + str], +): + """ + Valid refresh token returns new access token + """ + _, raw_token = refresh_token_pair + + response = await client.post( + URL_REFRESH, + cookies = {"refresh_token": raw_token}, + ) + + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + + +@pytest.mark.asyncio +async def test_refresh_token_missing_returns_401(client: AsyncClient): + """ + Missing refresh token cookie returns 401, not 422. + """ + response = await client.post(URL_REFRESH) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_refresh_token_expired( + client: AsyncClient, + expired_refresh_token_pair: tuple[RefreshToken, + str], +): + """ + Expired refresh token returns 401 + """ + _, raw_token = expired_refresh_token_pair + + response = await client.post( + URL_REFRESH, + cookies = {"refresh_token": raw_token}, + ) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_refresh_token_revoked( + client: AsyncClient, + revoked_refresh_token_pair: tuple[RefreshToken, + str], +): + """ + Revoked refresh token returns 401. + """ + _, raw_token = revoked_refresh_token_pair + + response = await client.post( + URL_REFRESH, + cookies = {"refresh_token": raw_token}, + ) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_logout_success( + client: AsyncClient, + refresh_token_pair: tuple[RefreshToken, + str], +): + """ + Logout revokes refresh token and clears cookie. + """ + _, raw_token = refresh_token_pair + + response = await client.post( + URL_LOGOUT, + cookies = {"refresh_token": raw_token}, + ) + + assert response.status_code == 204 + + +@pytest.mark.asyncio +async def test_logout_missing_token_returns_401(client: AsyncClient): + """ + Logout without refresh token returns 401, not 422. + """ + response = await client.post(URL_LOGOUT) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_logout_all( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Logout all revokes all user sessions. + """ + response = await client.post( + URL_LOGOUT_ALL, + headers = auth_headers, + ) + + assert response.status_code == 200 + data = response.json() + assert "revoked_sessions" in data + + +@pytest.mark.asyncio +async def test_get_current_user( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + /me returns current authenticated user. + """ + response = await client.get( + URL_ME, + headers = auth_headers, + ) + + assert response.status_code == 200 + data = response.json() + assert data["email"] == test_user.email + assert data["id"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_get_current_user_unauthenticated(client: AsyncClient): + """ + /me without auth returns 401. + """ + response = await client.get(URL_ME) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_change_password( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Password change works with valid current password. + """ + response = await client.post( + URL_CHANGE_PASSWORD, + headers = auth_headers, + json = { + "current_password": "TestPass123", + "new_password": "NewTestPass456", + }, + ) + + assert response.status_code == 204 + + +@pytest.mark.asyncio +async def test_change_password_wrong_current( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Password change fails with wrong current password. + """ + response = await client.post( + URL_CHANGE_PASSWORD, + headers = auth_headers, + json = { + "current_password": "WrongPassword123", + "new_password": "NewTestPass456", + }, + ) + + assert response.status_code == 401 diff --git a/PROJECTS/bug-bounty-platform/backend/tests/integration/test_health.py b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_health.py new file mode 100644 index 00000000..78f69cd3 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_health.py @@ -0,0 +1,40 @@ +""" +©AngelaMos | 2025 +test_health.py +""" + +import pytest +from httpx import AsyncClient + + +URL_HEALTH = "/health" +URL_HEALTH_DETAILED = "/health/detailed" + + +@pytest.mark.asyncio +async def test_health_basic(client: AsyncClient): + """ + Basic health check returns 200 with healthy status + """ + response = await client.get(URL_HEALTH) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert "environment" in data + assert "version" in data + + +@pytest.mark.asyncio +async def test_health_detailed(client: AsyncClient): + """ + Detailed health check includes database status + """ + response = await client.get(URL_HEALTH_DETAILED) + + assert response.status_code == 200 + data = response.json() + assert data["status"] in ["healthy", "degraded"] + assert "database" in data + assert "environment" in data + assert "version" in data diff --git a/PROJECTS/bug-bounty-platform/backend/tests/integration/test_users.py b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_users.py new file mode 100644 index 00000000..46a0800f --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/integration/test_users.py @@ -0,0 +1,240 @@ +""" +©AngelaMos | 2025 +test_users.py +""" + +import pytest +from httpx import AsyncClient + +from user.User import User + + +URL_USERS = "/v1/users" +URL_ADMIN_USERS = "/v1/admin/users" +URL_USER_ME = "/v1/users/me" + + +def url_user_by_id(user_id: str) -> str: + return f"{URL_USERS}/{user_id}" + + +def url_admin_user_by_id(user_id: str) -> str: + return f"{URL_ADMIN_USERS}/{user_id}" + + +@pytest.mark.asyncio +async def test_create_user(client: AsyncClient): + """ + User registration creates new user + """ + response = await client.post( + URL_USERS, + json = { + "email": "newuser@test.com", + "password": "ValidPass123", + "full_name": "New User", + }, + ) + + assert response.status_code == 201 + data = response.json() + assert data["email"] == "newuser@test.com" + assert data["full_name"] == "New User" + assert "id" in data + assert "hashed_password" not in data + + +@pytest.mark.asyncio +async def test_create_user_duplicate_email( + client: AsyncClient, + test_user: User +): + """ + Duplicate email returns 409 conflict + """ + response = await client.post( + URL_USERS, + json = { + "email": test_user.email, + "password": "ValidPass123", + }, + ) + + assert response.status_code == 409 + + +@pytest.mark.asyncio +async def test_create_user_weak_password(client: AsyncClient): + """ + Weak password (no uppercase/digit) returns 422 + """ + response = await client.post( + URL_USERS, + json = { + "email": "weakpass@test.com", + "password": "weakpassword", + }, + ) + + assert response.status_code == 422 + + +@pytest.mark.asyncio +async def test_get_user_by_id( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Get user by ID returns user data + """ + response = await client.get( + url_user_by_id(str(test_user.id)), + headers = auth_headers, + ) + + assert response.status_code == 200 + data = response.json() + assert data["email"] == test_user.email + assert data["id"] == str(test_user.id) + + +@pytest.mark.asyncio +async def test_get_user_unauthenticated( + client: AsyncClient, + test_user: User +): + """ + Get user without auth returns 401 + """ + response = await client.get(url_user_by_id(str(test_user.id))) + + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_get_user_not_found( + client: AsyncClient, + auth_headers: dict[str, + str], +): + """ + Get non existent user returns 404 + """ + fake_id = "00000000-0000-0000-0000-000000000000" + response = await client.get( + url_user_by_id(fake_id), + headers = auth_headers, + ) + + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_update_current_user( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Update current user profile + """ + response = await client.patch( + URL_USER_ME, + headers = auth_headers, + json = {"full_name": "Updated Name"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["full_name"] == "Updated Name" + + +@pytest.mark.asyncio +async def test_update_user_clear_field( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Setting field to null clears it + """ + response = await client.patch( + URL_USER_ME, + headers = auth_headers, + json = {"full_name": None}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["full_name"] is None + + +@pytest.mark.asyncio +async def test_list_users_admin_only( + client: AsyncClient, + test_user: User, + auth_headers: dict[str, + str], +): + """ + Non admin cannot list users (403). + """ + response = await client.get( + URL_ADMIN_USERS, + headers = auth_headers, + ) + + assert response.status_code == 403 + + +@pytest.mark.asyncio +async def test_list_users_as_admin( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, + str], +): + """ + Admin can list users with pagination + """ + response = await client.get( + URL_ADMIN_USERS, + headers = admin_auth_headers, + ) + + assert response.status_code == 200 + data = response.json() + assert "items" in data + assert "total" in data + assert "page" in data + assert "size" in data + assert isinstance(data["items"], list) + + +@pytest.mark.asyncio +async def test_list_users_pagination( + client: AsyncClient, + admin_user: User, + admin_auth_headers: dict[str, + str], +): + """ + Pagination params work correctly + """ + response = await client.get( + URL_ADMIN_USERS, + headers = admin_auth_headers, + params = { + "page": 1, + "size": 5 + }, + ) + + assert response.status_code == 200 + data = response.json() + assert data["page"] == 1 + assert data["size"] == 5 diff --git a/PROJECTS/bug-bounty-platform/backend/tests/unit/__init__.py b/PROJECTS/bug-bounty-platform/backend/tests/unit/__init__.py new file mode 100644 index 00000000..7fc7366b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/tests/unit/__init__.py @@ -0,0 +1,4 @@ +""" +AngelaMos | 2025 +__init__.py +""" diff --git a/PROJECTS/bug-bounty-platform/backend/uv.lock b/PROJECTS/bug-bounty-platform/backend/uv.lock new file mode 100644 index 00000000..800e0454 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/backend/uv.lock @@ -0,0 +1,2068 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "alembic" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "asgi-lifespan" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/da/e7908b54e0f8043725a990bf625f2041ecf6bfe8eb7b19407f1c00b630f7/asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308", size = 15627, upload-time = "2023-03-28T17:35:49.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + +[[package]] +name = "bug-bounty-platform" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "alembic" }, + { name = "asyncpg" }, + { name = "fastapi", extra = ["standard"] }, + { name = "gunicorn" }, + { name = "psycopg2-binary" }, + { name = "pwdlib", extra = ["argon2"] }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-multipart" }, + { name = "redis" }, + { name = "slowapi" }, + { name = "sqlalchemy" }, + { name = "structlog" }, + { name = "uuid6" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +dev = [ + { name = "aiosqlite" }, + { name = "asgi-lifespan" }, + { name = "httpx" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pylint" }, + { name = "pylint-per-file-ignores" }, + { name = "pylint-pydantic" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "ty" }, + { name = "types-redis" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiosqlite", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "alembic", specifier = ">=1.17.0,<2.0.0" }, + { name = "asgi-lifespan", marker = "extra == 'dev'", specifier = ">=2.1.0" }, + { name = "asyncpg", specifier = ">=0.31.0,<1.0.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.123.0,<1.0.0" }, + { name = "gunicorn", specifier = ">=23.0.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.1" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.2.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" }, + { name = "pydantic", specifier = ">=2.12.5,<3.0.0" }, + { name = "pydantic-settings", specifier = ">=2.12.0,<3.0.0" }, + { name = "pyjwt", specifier = ">=2.10.0" }, + { name = "pylint", marker = "extra == 'dev'", specifier = ">=4.0.4" }, + { name = "pylint-per-file-ignores", marker = "extra == 'dev'", specifier = ">=3.2.0" }, + { name = "pylint-pydantic", marker = "extra == 'dev'", specifier = ">=0.4.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "redis", specifier = ">=7.1.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.8" }, + { name = "slowapi", specifier = ">=0.1.9" }, + { name = "sqlalchemy", specifier = ">=2.0.44,<3.0.0" }, + { name = "structlog", specifier = ">=24.4.0" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a32" }, + { name = "types-redis", marker = "extra == 'dev'", specifier = ">=4.6.0" }, + { name = "uuid6", specifier = ">=2025.0.1" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/5d/3b33438de35521fab4968b232caa9a4bd568a5078f2b2dfb7bb8a4528603/fastapi_cloud_cli-0.8.0.tar.gz", hash = "sha256:cf07c502528bfd9e6b184776659f05d9212811d76bbec9fbb6bf34bed4c7456f", size = 30257, upload-time = "2025-12-23T12:08:33.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/8e/abb95ef59e91bb5adaa2d18fbf9ea70fd524010bb03f406a2dd2a4775ef9/fastapi_cloud_cli-0.8.0-py3-none-any.whl", hash = "sha256:e9f40bee671d985fd25d7a5409b56d4f103777bf8a0c6d746ea5fbf97a8186d9", size = 22306, upload-time = "2025-12-23T12:08:32.68Z" }, +] + +[[package]] +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, + { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, + { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, + { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "limits" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/e5/c968d43a65128cd54fb685f257aafb90cd5e4e1c67d084a58f0e4cbed557/limits-5.6.0.tar.gz", hash = "sha256:807fac75755e73912e894fdd61e2838de574c5721876a19f7ab454ae1fffb4b5", size = 182984, upload-time = "2025-09-29T17:15:22.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/96/4fcd44aed47b8fcc457653b12915fcad192cd646510ef3f29fd216f4b0ab/limits-5.6.0-py3-none-any.whl", hash = "sha256:b585c2104274528536a5b68864ec3835602b3c4a802cd6aa0b07419798394021", size = 60604, upload-time = "2025-09-29T17:15:18.419Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + +[[package]] +name = "pwdlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/41/a7c0d8a003c36ce3828ae3ed0391fe6a15aad65f082dbd6bec817ea95c0b/pwdlib-0.3.0.tar.gz", hash = "sha256:6ca30f9642a1467d4f5d0a4d18619de1c77f17dfccb42dd200b144127d3c83fc", size = 215810, upload-time = "2025-10-25T12:44:24.395Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/0c/9086a357d02a050fbb3270bf5043ac284dbfb845670e16c9389a41defc9e/pwdlib-0.3.0-py3-none-any.whl", hash = "sha256:f86c15c138858c09f3bba0a10984d4f9178158c55deaa72eac0210849b1a140d", size = 8633, upload-time = "2025-10-25T12:44:23.406Z" }, +] + +[package.optional-dependencies] +argon2 = [ + { name = "argon2-cffi" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" }, +] + +[[package]] +name = "pylint-per-file-ignores" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pylint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/b2/cf916c3c8127282f60927a3fd382ef8e477e6ef090b3d1f1fedd62bff916/pylint_per_file_ignores-3.2.0.tar.gz", hash = "sha256:5eb30b2b64c49ca616b8940346b8b5b4973eeaa15700840c8b81a4b8ba565a02", size = 63854, upload-time = "2025-11-25T14:13:14.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/6a/09cbda0032e1040eea8c7daec3994e6d32b3edc26a81d226fd643537886b/pylint_per_file_ignores-3.2.0-py3-none-any.whl", hash = "sha256:8b995b7486f6652f942cf5721e24c29b72735fa911b6d22b65b2f87bad323590", size = 5576, upload-time = "2025-11-25T14:13:13.208Z" }, +] + +[[package]] +name = "pylint-plugin-utils" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pylint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/85/24eaf5d0d078fc8799ae6d89faf326d6e4d27d862fc9a710a52ab07b7bb5/pylint_plugin_utils-0.9.0.tar.gz", hash = "sha256:5468d763878a18d5cc4db46eaffdda14313b043c962a263a7d78151b90132055", size = 10474, upload-time = "2025-06-24T07:14:00.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/c9/a3b871b0b590c49e38884af6dab58ab9711053bd5c39b8899b72e367b9f6/pylint_plugin_utils-0.9.0-py3-none-any.whl", hash = "sha256:16e9b84e5326ba893a319a0323fcc8b4bcc9c71fc654fcabba0605596c673818", size = 11129, upload-time = "2025-06-24T07:13:58.993Z" }, +] + +[[package]] +name = "pylint-pydantic" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "pylint" }, + { name = "pylint-plugin-utils" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/ac/5de3c91c7f9354444af251f053cc9953c89cce1defa74b907f67be4f770a/pylint_pydantic-0.4.1-py3-none-any.whl", hash = "sha256:d1b937abe5c346d38de69ee1ada80c93d38ee2356addbabb687e2eb44036ac93", size = 16161, upload-time = "2025-10-27T08:03:33.641Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f0/0e9dc590513d5e742d7799e2038df3a05167cba084c6ca4f3cdd75b55164/sentry_sdk-2.48.0.tar.gz", hash = "sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473", size = 384828, upload-time = "2025-12-16T14:55:41.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/19/8d77f9992e5cbfcaa9133c3bf63b4fbbb051248802e1e803fed5c552fbb2/sentry_sdk-2.48.0-py2.py3-none-any.whl", hash = "sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172", size = 414555, upload-time = "2025-12-16T14:55:40.152Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "slowapi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "limits" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "structlog" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "ty" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/7b/4f677c622d58563c593c32081f8a8572afd90e43dc15b0dedd27b4305038/ty-0.0.9.tar.gz", hash = "sha256:83f980c46df17586953ab3060542915827b43c4748a59eea04190c59162957fe", size = 4858642, upload-time = "2026-01-05T12:24:56.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/3f/c1ee119738b401a8081ff84341781122296b66982e5982e6f162d946a1ff/ty-0.0.9-py3-none-linux_armv6l.whl", hash = "sha256:dd270d4dd6ebeb0abb37aee96cbf9618610723677f500fec1ba58f35bfa8337d", size = 9763596, upload-time = "2026-01-05T12:24:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/63/41/6b0669ef4cd806d4bd5c30263e6b732a362278abac1bc3a363a316cde896/ty-0.0.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:debfb2ba418b00e86ffd5403cb666b3f04e16853f070439517dd1eaaeeff9255", size = 9591514, upload-time = "2026-01-05T12:24:26.891Z" }, + { url = "https://files.pythonhosted.org/packages/02/a1/874aa756aee5118e690340a771fb9ded0d0c2168c0b7cc7d9561c2a750b0/ty-0.0.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:107c76ebb05a13cdb669172956421f7ffd289ad98f36d42a44a465588d434d58", size = 9097773, upload-time = "2026-01-05T12:24:14.442Z" }, + { url = "https://files.pythonhosted.org/packages/32/62/cb9a460cf03baab77b3361d13106b93b40c98e274d07c55f333ce3c716f6/ty-0.0.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6868ca5c87ca0caa1b3cb84603c767356242b0659b88307eda69b2fb0bfa416b", size = 9581824, upload-time = "2026-01-05T12:24:35.074Z" }, + { url = "https://files.pythonhosted.org/packages/5a/97/633ecb348c75c954f09f8913669de8c440b13b43ea7d214503f3f1c4bb60/ty-0.0.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14a4aa0eb5c1d3591c2adbdda4e44429a6bb5d2e298a704398bb2a7ccdafdfe", size = 9591050, upload-time = "2026-01-05T12:24:08.804Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/4b0c6a7a8a234e2113f88c80cc7aaa9af5868de7a693859f3c49da981934/ty-0.0.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01bd4466504cefa36b465c6608e9af4504415fa67f6affc01c7d6ce36663c7f4", size = 10018262, upload-time = "2026-01-05T12:24:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/cb/97/076d72a028f6b31e0b87287aa27c5b71a2f9927ee525260ea9f2f56828b8/ty-0.0.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:76c8253d1b30bc2c3eaa1b1411a1c34423decde0f4de0277aa6a5ceacfea93d9", size = 10911642, upload-time = "2026-01-05T12:24:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/705d6a5ed07ea36b1f23592c3f0dbc8fc7649267bfbb3bf06464cdc9a98a/ty-0.0.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8992fa4a9c6a5434eae4159fdd4842ec8726259bfd860e143ab95d078de6f8e3", size = 10632468, upload-time = "2026-01-05T12:24:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/44/78/4339a254537488d62bf392a936b3ec047702c0cc33d6ce3a5d613f275cd0/ty-0.0.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c79d503d151acb4a145a3d98702d07cb641c47292f63e5ffa0151e4020a5d33", size = 10273422, upload-time = "2026-01-05T12:24:45.8Z" }, + { url = "https://files.pythonhosted.org/packages/90/40/e7f386e87c9abd3670dcee8311674d7e551baa23b2e4754e2405976e6c92/ty-0.0.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a7ebf89ed276b564baa1f0dd9cd708e7b5aa89f19ce1b2f7d7132075abf93e", size = 10120289, upload-time = "2026-01-05T12:24:17.424Z" }, + { url = "https://files.pythonhosted.org/packages/f7/46/1027442596e725c50d0d1ab5179e9fa78a398ab412994b3006d0ee0899c7/ty-0.0.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ae3866e50109d2400a886bb11d9ef607f23afc020b226af773615cf82ae61141", size = 9566657, upload-time = "2026-01-05T12:24:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/df921cf1967226aa01690152002b370a7135c6cced81e86c12b86552cdc4/ty-0.0.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:185244a5eacfcd8f5e2d85b95e4276316772f1e586520a6cb24aa072ec1bac26", size = 9610334, upload-time = "2026-01-05T12:24:20.334Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e8/f085268860232cc92ebe95415e5c8640f7f1797ac3a49ddd137c6222924d/ty-0.0.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f834ff27d940edb24b2e86bbb3fb45ab9e07cf59ca8c5ac615095b2542786408", size = 9726701, upload-time = "2026-01-05T12:24:29.785Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/9394210c66041cd221442e38f68a596945103d9446ece505889ffa9b3da9/ty-0.0.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:773f4b3ba046de952d7c1ad3a2c09b24f3ed4bc8342ae3cbff62ebc14aa6d48c", size = 10227082, upload-time = "2026-01-05T12:24:40.132Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9f/75951eb573b473d35dd9570546fc1319f7ca2d5b5c50a5825ba6ea6cb33a/ty-0.0.9-py3-none-win32.whl", hash = "sha256:1f20f67e373038ff20f36d5449e787c0430a072b92d5933c5b6e6fc79d3de4c8", size = 9176458, upload-time = "2026-01-05T12:24:32.559Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/b1cdf71ac874e72678161e25e2326a7d30bc3489cd3699561355a168e54f/ty-0.0.9-py3-none-win_amd64.whl", hash = "sha256:2c415f3bbb730f8de2e6e0b3c42eb3a91f1b5fbbcaaead2e113056c3b361c53c", size = 10040479, upload-time = "2026-01-05T12:24:42.697Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8f/abc75c4bb774b12698629f02d0d12501b0a7dff9c31dc3bd6b6c6467e90a/ty-0.0.9-py3-none-win_arm64.whl", hash = "sha256:48e339d794542afeed710ea4f846ead865cc38cecc335a9c781804d02eaa2722", size = 9543127, upload-time = "2026-01-05T12:24:11.731Z" }, +] + +[[package]] +name = "typer" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" }, +] + +[[package]] +name = "types-cffi" +version = "1.17.0.20250915" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/98/ea454cea03e5f351323af6a482c65924f3c26c515efd9090dede58f2b4b6/types_cffi-1.17.0.20250915.tar.gz", hash = "sha256:4362e20368f78dabd5c56bca8004752cc890e07a71605d9e0d9e069dbaac8c06", size = 17229, upload-time = "2025-09-15T03:01:25.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/ec/092f2b74b49ec4855cdb53050deb9699f7105b8fda6fe034c0781b8687f3/types_cffi-1.17.0.20250915-py3-none-any.whl", hash = "sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c", size = 20112, upload-time = "2025-09-15T03:01:24.187Z" }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679, upload-time = "2024-10-04T02:43:59.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737, upload-time = "2024-10-04T02:43:57.968Z" }, +] + +[[package]] +name = "types-setuptools" +version = "80.9.0.20251223" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/07/d1b605230730990de20477150191d6dccf6aecc037da94c9960a5d563bc8/types_setuptools-80.9.0.20251223.tar.gz", hash = "sha256:d3411059ae2f5f03985217d86ac6084efea2c9e9cacd5f0869ef950f308169b2", size = 42420, upload-time = "2025-12-23T03:18:26.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/5c/b8877da94012dbc6643e4eeca22bca9b99b295be05d161f8a403ae9387c0/types_setuptools-80.9.0.20251223-py3-none-any.whl", hash = "sha256:1b36db79d724c2287d83dc052cf887b47c0da6a2fff044378be0b019545f56e6", size = 64318, upload-time = "2025-12-23T03:18:25.868Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "uuid6" +version = "2025.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/b7/4c0f736ca824b3a25b15e8213d1bcfc15f8ac2ae48d1b445b310892dc4da/uuid6-2025.0.1.tar.gz", hash = "sha256:cd0af94fa428675a44e32c5319ec5a3485225ba2179eefcf4c3f205ae30a81bd", size = 13932, upload-time = "2025-07-04T18:30:35.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b2/93faaab7962e2aa8d6e174afb6f76be2ca0ce89fde14d3af835acebcaa59/uuid6-2025.0.1-py3-none-any.whl", hash = "sha256:80530ce4d02a93cdf82e7122ca0da3ebbbc269790ec1cb902481fa3e9cc9ff99", size = 6979, upload-time = "2025-07-04T18:30:34.001Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] diff --git a/PROJECTS/bug-bounty-platform/compose.yml b/PROJECTS/bug-bounty-platform/compose.yml new file mode 100644 index 00000000..2c05e5b2 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/compose.yml @@ -0,0 +1,158 @@ +# ========================================= +# AngelaMos | 2026 +# Production | compose.yml +# ========================================= + + +name: ${APP_NAME:-template} + +services: + # Nginx + Frontend + nginx: + build: + context: . + dockerfile: infra/docker/frontend-builder.prod + args: + - VITE_API_URL=${VITE_API_URL:-/api} + - VITE_APP_TITLE=${VITE_APP_TITLE:-My App} + container_name: ${APP_NAME:-template}-nginx + ports: + - "${NGINX_HOST_PORT:-8420}:80" + depends_on: + backend: + condition: service_healthy + networks: + - frontend + - backend + deploy: + resources: + limits: + cpus: '1.0' + memory: 256M + reservations: + cpus: '0.25' + memory: 64M + restart: unless-stopped + + # FastAPI backend + backend: + build: + context: ./backend + dockerfile: ../infra/docker/fastapi.prod + container_name: ${APP_NAME:-template}-backend + expose: + - "8000" + env_file: + - .env + environment: + - ENVIRONMENT=production + - DEBUG=false + - RELOAD=false + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - backend + deploy: + resources: + limits: + cpus: '2.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 256M + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 40s + restart: unless-stopped + + # PostgreSQL DB + db: + image: postgres:18-alpine + container_name: ${APP_NAME:-template}-db + ports: + - "${POSTGRES_HOST_PORT:-3420}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - POSTGRES_DB=${POSTGRES_DB:-app_db} + networks: + - backend + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-app_db}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + + # Redis + redis: + image: redis:7-alpine + container_name: ${APP_NAME:-template}-redis + ports: + - "${REDIS_HOST_PORT:-6420}:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}} + networks: + - backend + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.1' + memory: 64M + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # Cloudflare Tunnel + cloudflared: + image: cloudflare/cloudflared:latest + container_name: ${APP_NAME:-template}-tunnel + command: tunnel run --token ${CLOUDFLARE_TUNNEL_TOKEN} + networks: + - backend + depends_on: + nginx: + condition: service_started + deploy: + resources: + limits: + cpus: '0.5' + memory: 128M + reservations: + cpus: '0.1' + memory: 32M + restart: unless-stopped + +networks: + frontend: + driver: bridge + backend: + driver: bridge + +volumes: + postgres_data: + redis_data: diff --git a/PROJECTS/bug-bounty-platform/dev.compose.yml b/PROJECTS/bug-bounty-platform/dev.compose.yml new file mode 100644 index 00000000..980ede82 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/dev.compose.yml @@ -0,0 +1,128 @@ +# ============================================================================= +# AngelaMos | 2026 +# dev.compose.yml +# ============================================================================= + +name: ${APP_NAME:-template}-dev + +services: + # Nginx + nginx: + image: nginx:1.27-alpine + container_name: ${APP_NAME:-template}-nginx-dev + ports: + - "${NGINX_HOST_PORT:-8420}:80" + volumes: + - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./infra/nginx/dev.nginx:/etc/nginx/conf.d/default.conf:ro + depends_on: + backend: + condition: service_healthy + frontend: + condition: service_started + networks: + - frontend + - backend + restart: unless-stopped + + # FastAPI + backend: + build: + context: ./backend + dockerfile: ../infra/docker/fastapi.dev + container_name: ${APP_NAME:-template}-backend-dev + ports: + - "${BACKEND_HOST_PORT:-5420}:8000" + volumes: + - ./backend:/app + - backend_cache:/app/.venv + env_file: + - .env + environment: + - ENVIRONMENT=development + - DEBUG=true + - RELOAD=true + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + networks: + - backend + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + + # Vite dev server + frontend: + build: + context: ./frontend + dockerfile: ../infra/docker/vite.dev + container_name: ${APP_NAME:-template}-frontend-dev + ports: + - "${FRONTEND_HOST_PORT:-3420}:5173" + volumes: + - ./frontend:/app + - frontend_modules:/app/node_modules + environment: + - VITE_API_URL=${VITE_API_URL:-/api} + - VITE_APP_TITLE=${VITE_APP_TITLE:-My App} + networks: + - frontend + restart: unless-stopped + + # PostgreSQL DB + db: + image: postgres:16-alpine + container_name: ${APP_NAME:-template}-db-dev + ports: + - "${POSTGRES_HOST_PORT:-3420}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - POSTGRES_DB=${POSTGRES_DB:-app_db} + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-app_db}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + restart: unless-stopped + + # Redis + redis: + image: redis:7-alpine + container_name: ${APP_NAME:-template}-redis-dev + ports: + - "${REDIS_HOST_PORT:-6420}:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + networks: + - backend + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +networks: + frontend: + driver: bridge + backend: + driver: bridge + +volumes: + postgres_data: + redis_data: + backend_cache: + frontend_modules: diff --git a/PROJECTS/bug-bounty-platform/frontend/.dockerignore b/PROJECTS/bug-bounty-platform/frontend/.dockerignore new file mode 100644 index 00000000..a0256ec7 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +build +dist +.git +.gitignore +*.md +.env* +.vscode +.idea +*.log +npm-debug.log* +pnpm-debug.log* +.DS_Store +coverage +.nyc_output diff --git a/PROJECTS/bug-bounty-platform/frontend/.gitignore b/PROJECTS/bug-bounty-platform/frontend/.gitignore new file mode 100644 index 00000000..61cb0c2e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/PROJECTS/bug-bounty-platform/frontend/.stylelintignore b/PROJECTS/bug-bounty-platform/frontend/.stylelintignore new file mode 100755 index 00000000..1ae0ac7c --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/.stylelintignore @@ -0,0 +1,22 @@ +# ©AngelaMos | 2026 +# .stylelintignore + +# Dependencies +node_modules/ + +# Production builds +dist/ +build/ +out/ + +# JS/TS files +**/*.js +**/*.jsx +**/*.ts +**/*.tsx + +# Generated files +*.min.css + +# Error system styles - ignore from linting +src/core/app/_toastStyles.scss diff --git a/PROJECTS/bug-bounty-platform/frontend/biome.json b/PROJECTS/bug-bounty-platform/frontend/biome.json new file mode 100644 index 00000000..fae39124 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/biome.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 82, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "semicolons": "asNeeded", + "trailingCommas": "es5", + "arrowParentheses": "always" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noExcessiveCognitiveComplexity": { + "level": "error", + "options": { "maxAllowedComplexity": 25 } + }, + "noForEach": "off", + "useLiteralKeys": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "noUndeclaredVariables": "error" + }, + "style": { + "useImportType": "error", + "useConst": "error", + "useTemplate": "error", + "useSelfClosingElements": "error", + "useFragmentSyntax": "error", + "noNonNullAssertion": "error", + "useConsistentArrayType": { + "level": "error", + "options": { "syntax": "shorthand" } + }, + "useNamingConvention": "off" + }, + "suspicious": { + "noExplicitAny": "error", + "noDebugger": "error", + "noConsole": "warn", + "noArrayIndexKey": "warn", + "noAssignInExpressions": "error", + "noDoubleEquals": "error", + "noRedeclare": "error", + "noVar": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "error" + }, + "a11y": { + "useAltText": "error", + "useAnchorContent": "error", + "useKeyWithClickEvents": "error", + "noStaticElementInteractions": "error", + "useButtonType": "error", + "useValidAnchor": "error" + } + } + }, + "overrides": [ + { + "includes": ["src/main.tsx"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] +} diff --git a/PROJECTS/bug-bounty-platform/frontend/index.html b/PROJECTS/bug-bounty-platform/frontend/index.html new file mode 100644 index 00000000..65ee0f76 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + Bug Bounty Platform + + + + +
+ + + diff --git a/PROJECTS/bug-bounty-platform/frontend/package.json b/PROJECTS/bug-bounty-platform/frontend/package.json new file mode 100644 index 00000000..388a2c2d --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "bug-bounty-platform", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc --noEmit", + "lint:scss": "stylelint '**/*.scss'", + "lint:scss:fix": "stylelint '**/*.scss' --fix" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "axios": "^1.13.0", + "react": "^19.2.1", + "react-dom": "^19.2.0", + "react-error-boundary": "^6.0.0", + "react-icon": "^1.0.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.1.1", + "sonner": "^2.0.7", + "zod": "^4.1.13", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.8", + "@tanstack/react-query-devtools": "^5.91.1", + "@types/node": "^24.10.2", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "sass": "^1.95.0", + "stylelint": "^16.26.1", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^16.0.0", + "typescript": "~5.9.3", + "vite": "npm:rolldown-vite@7.2.5", + "vite-tsconfig-paths": "^5.1.0" + }, + "pnpm": { + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/pnpm-lock.yaml b/PROJECTS/bug-bounty-platform/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..0b8c1441 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/pnpm-lock.yaml @@ -0,0 +1,2598 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + vite: npm:rolldown-vite@7.2.5 + +importers: + + .: + dependencies: + '@tanstack/react-query': + specifier: ^5.90.12 + version: 5.90.16(react@19.2.3) + axios: + specifier: ^1.13.0 + version: 1.13.2 + react: + specifier: ^19.2.1 + version: 19.2.3 + react-dom: + specifier: ^19.2.0 + version: 19.2.3(react@19.2.3) + react-error-boundary: + specifier: ^6.0.0 + version: 6.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-icon: + specifier: ^1.0.0 + version: 1.0.0(babel-runtime@5.8.38)(react@19.2.3) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.2.3) + react-router-dom: + specifier: ^7.1.1 + version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + zod: + specifier: ^4.1.13 + version: 4.3.5 + zustand: + specifier: ^5.0.9 + version: 5.0.9(@types/react@19.2.7)(react@19.2.3) + devDependencies: + '@biomejs/biome': + specifier: ^2.3.8 + version: 2.3.11 + '@tanstack/react-query-devtools': + specifier: ^5.91.1 + version: 5.91.2(@tanstack/react-query@5.90.16(react@19.2.3))(react@19.2.3) + '@types/node': + specifier: ^24.10.2 + version: 24.10.4 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.2(rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2)) + sass: + specifier: ^1.95.0 + version: 1.97.2 + stylelint: + specifier: ^16.26.1 + version: 16.26.1(typescript@5.9.3) + stylelint-config-prettier-scss: + specifier: ^1.0.0 + version: 1.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard-scss: + specifier: ^16.0.0 + version: 16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: npm:rolldown-vite@7.2.5 + version: rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2) + vite-tsconfig-paths: + specifier: ^5.1.0 + version: 5.1.4(rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2))(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.3.11': + resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.11': + resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.11': + resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.11': + resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.11': + resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.11': + resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.11': + resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@2.3.11': + resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.11': + resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cacheable/memory@2.0.7': + resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==} + + '@cacheable/utils@2.3.3': + resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==} + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.22': + resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.0': + resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.5.4 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/runtime@0.97.0': + resolution: {integrity: sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.97.0': + resolution: {integrity: sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + resolution: {integrity: sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + resolution: {integrity: sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + resolution: {integrity: sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + resolution: {integrity: sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + resolution: {integrity: sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.50': + resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==} + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@tanstack/query-core@5.90.16': + resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==} + + '@tanstack/query-devtools@5.92.0': + resolution: {integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==} + + '@tanstack/react-query-devtools@5.91.2': + resolution: {integrity: sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==} + peerDependencies: + '@tanstack/react-query': ^5.90.14 + react: ^18 || ^19 + + '@tanstack/react-query@5.90.16': + resolution: {integrity: sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==} + peerDependencies: + react: ^18 || ^19 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + babel-runtime@5.8.38: + resolution: {integrity: sha512-KpgoA8VE/pMmNCrnEeeXqFG24TIH11Z3ZaimIhJWsin8EbfZy3WzFKUTIan10ZIDgRVvi9EkLbruJElJC9dRlg==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.3.1: + resolution: {integrity: sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@1.2.7: + resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.1: + resolution: {integrity: sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flat-cache@6.1.19: + resolution: {integrity: sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.4.0: + resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==} + engines: {node: '>=20'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookified@1.15.0: + resolution: {integrity: sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw==} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@5.5.5: + resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.25.0: + resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qified@0.5.3: + resolution: {integrity: sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ==} + engines: {node: '>=20'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-error-boundary@6.0.2: + resolution: {integrity: sha512-yvWErn55ag/ywZEFqYpXYX9rxIDPIabXIX25F184KY3F5Szk2x/cVieOflw5R47ltN3KzWOw82Lmlb4vNjyn9A==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + react-icon@1.0.0: + resolution: {integrity: sha512-VzSlpBHnLanVw79mOxyq98hWDi6DlxK9qPiZ1bAK6bLurMBCaxO/jjyYUrRx9+JGLc/NbnwOmyE/W5Qglbb2QA==} + peerDependencies: + babel-runtime: ^5.3.3 + react: '>=0.12.0' + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.11.0: + resolution: {integrity: sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.11.0: + resolution: {integrity: sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown-vite@7.2.5: + resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + esbuild: ^0.25.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + rolldown@1.0.0-beta.50: + resolution: {integrity: sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.97.2: + resolution: {integrity: sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + stylelint-config-prettier-scss@1.0.0: + resolution: {integrity: sha512-Gr2qLiyvJGKeDk0E/+awNTrZB/UtNVPLqCDOr07na/sLekZwm26Br6yYIeBYz3ulsEcQgs5j+2IIMXCC+wsaQA==} + engines: {node: 14.* || 16.* || >= 18} + hasBin: true + peerDependencies: + stylelint: '>=15.0.0' + + stylelint-config-recommended-scss@16.0.2: + resolution: {integrity: sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.24.0 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard-scss@16.0.0: + resolution: {integrity: sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA==} + engines: {node: '>=20'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.23.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-standard@39.0.1: + resolution: {integrity: sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-scss@6.14.0: + resolution: {integrity: sha512-ZKmHMZolxeuYsnB+PCYrTpFce0/QWX9i9gh0hPXzp73WjuIMqUpzdQaBCrKoLWh6XtCFSaNDErkMPqdjy1/8aA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.8.2 + + stylelint@16.26.1: + resolution: {integrity: sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==} + engines: {node: '>=18.12.0'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@biomejs/biome@2.3.11': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.11 + '@biomejs/cli-darwin-x64': 2.3.11 + '@biomejs/cli-linux-arm64': 2.3.11 + '@biomejs/cli-linux-arm64-musl': 2.3.11 + '@biomejs/cli-linux-x64': 2.3.11 + '@biomejs/cli-linux-x64-musl': 2.3.11 + '@biomejs/cli-win32-arm64': 2.3.11 + '@biomejs/cli-win32-x64': 2.3.11 + + '@biomejs/cli-darwin-arm64@2.3.11': + optional: true + + '@biomejs/cli-darwin-x64@2.3.11': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.11': + optional: true + + '@biomejs/cli-linux-arm64@2.3.11': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.11': + optional: true + + '@biomejs/cli-linux-x64@2.3.11': + optional: true + + '@biomejs/cli-win32-arm64@2.3.11': + optional: true + + '@biomejs/cli-win32-x64@2.3.11': + optional: true + + '@cacheable/memory@2.0.7': + dependencies: + '@cacheable/utils': 2.3.3 + '@keyv/bigmap': 1.3.0(keyv@5.5.5) + hookified: 1.15.0 + keyv: 5.5.5 + + '@cacheable/utils@2.3.3': + dependencies: + hashery: 1.4.0 + keyv: 5.5.5 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.22': {} + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.1)': + dependencies: + postcss-selector-parser: 7.1.1 + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.3.0(keyv@5.5.5)': + dependencies: + hashery: 1.4.0 + hookified: 1.15.0 + keyv: 5.5.5 + + '@keyv/serialize@1.1.1': {} + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/runtime@0.97.0': {} + + '@oxc-project/types@0.97.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rolldown/binding-android-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.50': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.50': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.50': {} + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@tanstack/query-core@5.90.16': {} + + '@tanstack/query-devtools@5.92.0': {} + + '@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.16(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/query-devtools': 5.92.0 + '@tanstack/react-query': 5.90.16(react@19.2.3) + react: 19.2.3 + + '@tanstack/react-query@5.90.16(react@19.2.3)': + dependencies: + '@tanstack/query-core': 5.90.16 + react: 19.2.3 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/node@24.10.4': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.7)': + dependencies: + '@types/react': 19.2.7 + + '@types/react@19.2.7': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2) + transitivePeerDependencies: + - supports-color + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + astral-regex@2.0.0: {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-runtime@5.8.38: + dependencies: + core-js: 1.2.7 + + balanced-match@2.0.0: {} + + baseline-browser-mapping@2.9.11: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001762 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + cacheable@2.3.1: + dependencies: + '@cacheable/memory': 2.0.7 + '@cacheable/utils': 2.3.3 + hookified: 1.15.0 + keyv: 5.5.5 + qified: 0.5.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001762: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + core-js@1.2.7: {} + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.267: {} + + emoji-regex@8.0.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escalade@3.2.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@11.1.1: + dependencies: + flat-cache: 6.1.19 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flat-cache@6.1.19: + dependencies: + cacheable: 2.3.1 + flatted: 3.3.3 + hookified: 1.15.0 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hashery@1.4.0: + dependencies: + hookified: 1.15.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookified@1.15.0: {} + + html-tags@3.3.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immutable@5.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + keyv@5.5.5: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.37.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + lodash.truncate@4.4.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.25.0: {} + + meow@13.2.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + qified@0.5.3: + dependencies: + hookified: 1.15.0 + + queue-microtask@1.2.3: {} + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-error-boundary@6.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + react-icon@1.0.0(babel-runtime@5.8.38)(react@19.2.3): + dependencies: + babel-runtime: 5.8.38 + react: 19.2.3 + + react-icons@5.5.0(react@19.2.3): + dependencies: + react: 19.2.3 + + react-refresh@0.18.0: {} + + react-router-dom@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-router: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + + react-router@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + cookie: 1.1.1 + react: 19.2.3 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + + react@19.2.3: {} + + readdirp@4.1.2: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2): + dependencies: + '@oxc-project/runtime': 0.97.0 + fdir: 6.5.0(picomatch@4.0.3) + lightningcss: 1.30.2 + picomatch: 4.0.3 + postcss: 8.5.6 + rolldown: 1.0.0-beta.50 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + sass: 1.97.2 + + rolldown@1.0.0-beta.50: + dependencies: + '@oxc-project/types': 0.97.0 + '@rolldown/pluginutils': 1.0.0-beta.50 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.50 + '@rolldown/binding-darwin-x64': 1.0.0-beta.50 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.50 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.50 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.50 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.50 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.50 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.50 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.50 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.97.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + stylelint-config-prettier-scss@1.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-recommended-scss@16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + stylelint-scss: 6.14.0(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended@17.0.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + + stylelint-config-standard-scss@16.0.0(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended-scss: 16.0.2(postcss@8.5.6)(stylelint@16.26.1(typescript@5.9.3)) + stylelint-config-standard: 39.0.1(stylelint@16.26.1(typescript@5.9.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-standard@39.0.1(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + stylelint: 16.26.1(typescript@5.9.3) + stylelint-config-recommended: 17.0.0(stylelint@16.26.1(typescript@5.9.3)) + + stylelint-scss@6.14.0(stylelint@16.26.1(typescript@5.9.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mdn-data: 2.25.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + stylelint: 16.26.1(typescript@5.9.3) + + stylelint@16.26.1(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-syntax-patches-for-csstree': 1.0.22 + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.1) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 11.1.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite-tsconfig-paths@5.1.4(rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2))(typescript@5.9.3): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: rolldown-vite@7.2.5(@types/node@24.10.4)(sass@1.97.2) + transitivePeerDependencies: + - supports-color + - typescript + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + yallist@3.1.1: {} + + zod@4.3.5: {} + + zustand@5.0.9(@types/react@19.2.7)(react@19.2.3): + optionalDependencies: + '@types/react': 19.2.7 + react: 19.2.3 diff --git a/PROJECTS/bug-bounty-platform/frontend/public/assets/android-chrome-192x192.png b/PROJECTS/bug-bounty-platform/frontend/public/assets/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..01d0a08ef524c65021a71f0c177b9d1317e7f986 GIT binary patch literal 25807 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%0Aq;P^m<|We$(VC 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 gF)%Qokv0KRi0n{r~^~ literal 0 HcmV?d00001 diff --git a/PROJECTS/bug-bounty-platform/frontend/public/assets/apple-touch-icon.png b/PROJECTS/bug-bounty-platform/frontend/public/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ec126db919dc679289e9ef69e842e82d8c5bbf5d GIT binary patch literal 22963 zcmXt9Q*Rqd)s z1vzm9I2^byU%ntnN{A?dUz`7Xz(9eYd8eS5zI;LcA}J!M;_>w&2ew;v0dv&$D`0PPa3J(vjqJ z2GNBU3lM7d~zX}rLC2w8RfQAHVxCaQyyqoOj+d z!#DAjcMPPG9xeARhQx9NGD zQ`-gpN32FeFtnPDB5zNZ(o`8OTXlUOXKWt3p_rW?k$Ho-f&01npC51bOEuc+=+tV- zlnE?e4;H+@4d+^1R;%Qzj$?wQaz$a&@#ML5M&so8cn1$s0;RV_WqDWS_Ap$ITW1YY zhmD^sA)MCBG#P9v&PS7y?#Q3d`$(==8>_URXH|8j+I&6V*4yj>jiK-?mkoNIj3aYE zX^b-ULlHkHF(Lr!!w2Y1w^pk_2$I!mQ9|DzhnPgNA+BgR=<~LN*o;qVzEx2-*%&2u|zt} zZ8)mO8))_Va4IzU-%{xW@jL7U@V%dqKAtX!r%*Mf9{h+Fs3o&# zzfVO{t1=&ur7HEIs`}aKZ_HZr=A@e0_A$+UeKLr|uX4LL{J3Ua2OE3NA*M>)Qdn5% zVwytbb?f-jP^Y`aDwpUWd;5}=WxLcEk%)alBiY8t#?r(>2^QT#SJxrVqj&PG+Z76h ztFT(C%w+KE-txJZ1s8`*p5JRlRP^859;e*TVN89C{vb%YU*Qp7cb#c#Kkw`MC_euB z)s>(7p8_jvRvYb}hh_@m1YYIuX4m=lSc%8qmuBYwoeaN<;yx68%txdHSzb3KUyt+r zm2?RU!CjR$^sNrO-JPeUSxPBm65rBq-(T)^zxnJ0LN~jd&Z8}PIMZmi%6x*Z{T~ + + + ) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/index.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/index.ts new file mode 100644 index 00000000..e04ac577 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/index.ts @@ -0,0 +1,10 @@ +// =================== +// AngelaMos | 2025 +// index.ts +// =================== + +export * from './useAdmin' +export * from './useAuth' +export * from './usePrograms' +export * from './useReports' +export * from './useUsers' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAdmin.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAdmin.ts new file mode 100644 index 00000000..2b86c9d5 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAdmin.ts @@ -0,0 +1,600 @@ +// =================== +// AngelaMos | 2026 +// useAdmin.ts +// =================== + +import { + type UseMutationResult, + type UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import { toast } from 'sonner' +import { + ADMIN_ERROR_MESSAGES, + ADMIN_SUCCESS_MESSAGES, + type AdminProgramListResponse, + type AdminProgramResponse, + type AdminProgramUpdate, + type AdminReportListResponse, + type AdminReportResponse, + type AdminReportUpdate, + AdminResponseError, + type AdminUserCreateRequest, + type AdminUserListResponse, + type AdminUserUpdateRequest, + isValidAdminProgramListResponse, + isValidAdminProgramResponse, + isValidAdminReportListResponse, + isValidAdminReportResponse, + isValidAdminUserListResponse, + isValidPlatformStatsResponse, + isValidUserListResponse, + isValidUserResponse, + type PlatformStatsResponse, + USER_ERROR_MESSAGES, + USER_SUCCESS_MESSAGES, + type UserListResponse, + type UserResponse, + UserResponseError, +} from '@/api/types' +import { API_ENDPOINTS, PAGINATION, QUERY_KEYS } from '@/config' +import { apiClient, QUERY_STRATEGIES } from '@/core/api' + +export const adminQueries = { + all: () => QUERY_KEYS.ADMIN.ALL, + stats: () => QUERY_KEYS.ADMIN.STATS(), + users: { + all: () => QUERY_KEYS.ADMIN.USERS.ALL(), + list: (page: number, size: number, role?: string) => + QUERY_KEYS.ADMIN.USERS.LIST(page, size, role), + byId: (id: string) => QUERY_KEYS.ADMIN.USERS.BY_ID(id), + }, + programs: { + all: () => QUERY_KEYS.ADMIN.PROGRAMS.ALL(), + list: (page: number, size: number, status?: string) => + QUERY_KEYS.ADMIN.PROGRAMS.LIST(page, size, status), + }, + reports: { + all: () => QUERY_KEYS.ADMIN.REPORTS.ALL(), + list: (page: number, size: number, status?: string, severity?: string) => + QUERY_KEYS.ADMIN.REPORTS.LIST(page, size, status, severity), + }, +} as const + +interface UseAdminUsersParams { + page?: number + size?: number +} + +const fetchAdminUsers = async ( + page: number, + size: number +): Promise => { + const response = await apiClient.get(API_ENDPOINTS.ADMIN.USERS.LIST, { + params: { page, size }, + }) + const data: unknown = response.data + + if (!isValidUserListResponse(data)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_LIST_RESPONSE, + API_ENDPOINTS.ADMIN.USERS.LIST + ) + } + + return data +} + +export const useAdminUsers = ( + params: UseAdminUsersParams = {} +): UseQueryResult => { + const page = params.page ?? PAGINATION.DEFAULT_PAGE + const size = params.size ?? PAGINATION.DEFAULT_SIZE + + return useQuery({ + queryKey: adminQueries.users.list(page, size), + queryFn: () => fetchAdminUsers(page, size), + ...QUERY_STRATEGIES.standard, + }) +} + +const fetchAdminUserById = async (id: string): Promise => { + const response = await apiClient.get( + API_ENDPOINTS.ADMIN.USERS.BY_ID(id) + ) + const data: unknown = response.data + + if (!isValidUserResponse(data)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.ADMIN.USERS.BY_ID(id) + ) + } + + return data +} + +export const useAdminUser = (id: string): UseQueryResult => { + return useQuery({ + queryKey: adminQueries.users.byId(id), + queryFn: () => fetchAdminUserById(id), + enabled: id.length > 0, + ...QUERY_STRATEGIES.standard, + }) +} + +const performAdminCreateUser = async ( + data: AdminUserCreateRequest +): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.ADMIN.USERS.CREATE, + data + ) + const responseData: unknown = response.data + + if (!isValidUserResponse(responseData)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.ADMIN.USERS.CREATE + ) + } + + return responseData +} + +export const useAdminCreateUser = (): UseMutationResult< + UserResponse, + Error, + AdminUserCreateRequest +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: performAdminCreateUser, + onSuccess: (newUser: UserResponse): void => { + queryClient.setQueriesData( + { queryKey: adminQueries.users.all() }, + (oldData) => { + if (!oldData) return oldData + return { + ...oldData, + items: [newUser, ...oldData.items], + total: oldData.total + 1, + } + } + ) + toast.success(USER_SUCCESS_MESSAGES.CREATED) + }, + onError: (error: Error): void => { + const message = + error instanceof UserResponseError + ? error.message + : USER_ERROR_MESSAGES.FAILED_TO_CREATE + toast.error(message) + }, + }) +} + +interface AdminUpdateUserParams { + id: string + data: AdminUserUpdateRequest +} + +const performAdminUpdateUser = async ( + params: AdminUpdateUserParams +): Promise => { + const response = await apiClient.patch( + API_ENDPOINTS.ADMIN.USERS.UPDATE(params.id), + params.data + ) + const responseData: unknown = response.data + + if (!isValidUserResponse(responseData)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.ADMIN.USERS.UPDATE(params.id) + ) + } + + return responseData +} + +export const useAdminUpdateUser = (): UseMutationResult< + UserResponse, + Error, + AdminUpdateUserParams +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: performAdminUpdateUser, + onSuccess: ( + updatedUser: UserResponse, + variables: AdminUpdateUserParams + ): void => { + queryClient.setQueryData(adminQueries.users.byId(variables.id), updatedUser) + queryClient.setQueriesData( + { queryKey: adminQueries.users.all() }, + (oldData) => { + if (!oldData) return oldData + return { + ...oldData, + items: oldData.items.map((user) => + user.id === updatedUser.id ? updatedUser : user + ), + } + } + ) + toast.success(USER_SUCCESS_MESSAGES.UPDATED) + }, + onError: (error: Error): void => { + const message = + error instanceof UserResponseError + ? error.message + : USER_ERROR_MESSAGES.FAILED_TO_UPDATE + toast.error(message) + }, + }) +} + +const performAdminDeleteUser = async (id: string): Promise => { + await apiClient.delete(API_ENDPOINTS.ADMIN.USERS.DELETE(id)) +} + +export const useAdminDeleteUser = (): UseMutationResult => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: performAdminDeleteUser, + onSuccess: (_, deletedId: string): void => { + queryClient.removeQueries({ queryKey: adminQueries.users.byId(deletedId) }) + queryClient.setQueriesData( + { queryKey: adminQueries.users.all() }, + (oldData) => { + if (!oldData) return oldData + return { + ...oldData, + items: oldData.items.filter((user) => user.id !== deletedId), + total: oldData.total - 1, + } + } + ) + toast.success(USER_SUCCESS_MESSAGES.DELETED) + }, + onError: (error: Error): void => { + const message = + error instanceof UserResponseError + ? error.message + : USER_ERROR_MESSAGES.FAILED_TO_DELETE + toast.error(message) + }, + }) +} + +const fetchPlatformStats = async (): Promise => { + const response = await apiClient.get(API_ENDPOINTS.ADMIN.STATS) + const data: unknown = response.data + + if (!isValidPlatformStatsResponse(data)) { + throw new AdminResponseError( + ADMIN_ERROR_MESSAGES.INVALID_STATS_RESPONSE, + API_ENDPOINTS.ADMIN.STATS + ) + } + + return data +} + +export const usePlatformStats = (): UseQueryResult< + PlatformStatsResponse, + Error +> => { + return useQuery({ + queryKey: adminQueries.stats(), + queryFn: fetchPlatformStats, + ...QUERY_STRATEGIES.standard, + }) +} + +interface UseAdminProgramsParams { + page?: number + size?: number + status?: string +} + +const fetchAdminPrograms = async ( + page: number, + size: number, + status?: string +): Promise => { + const params: Record = { page, size } + if (status) { + params.status = status + } + + const response = await apiClient.get( + API_ENDPOINTS.ADMIN.PROGRAMS.LIST, + { + params, + } + ) + const data: unknown = response.data + + if (!isValidAdminProgramListResponse(data)) { + throw new AdminResponseError( + ADMIN_ERROR_MESSAGES.INVALID_PROGRAM_LIST_RESPONSE, + API_ENDPOINTS.ADMIN.PROGRAMS.LIST + ) + } + + return data +} + +export const useAdminPrograms = ( + params: UseAdminProgramsParams = {} +): UseQueryResult => { + const page = params.page ?? PAGINATION.DEFAULT_PAGE + const size = params.size ?? PAGINATION.DEFAULT_SIZE + + return useQuery({ + queryKey: adminQueries.programs.list(page, size, params.status), + queryFn: () => fetchAdminPrograms(page, size, params.status), + ...QUERY_STRATEGIES.standard, + }) +} + +interface AdminUpdateProgramParams { + id: string + data: AdminProgramUpdate +} + +const performAdminUpdateProgram = async ( + params: AdminUpdateProgramParams +): Promise => { + const response = await apiClient.patch( + API_ENDPOINTS.ADMIN.PROGRAMS.UPDATE(params.id), + params.data + ) + const responseData: unknown = response.data + + if (!isValidAdminProgramResponse(responseData)) { + throw new AdminResponseError( + ADMIN_ERROR_MESSAGES.INVALID_PROGRAM_RESPONSE, + API_ENDPOINTS.ADMIN.PROGRAMS.UPDATE(params.id) + ) + } + + return responseData +} + +export const useAdminUpdateProgram = (): UseMutationResult< + AdminProgramResponse, + Error, + AdminUpdateProgramParams +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: performAdminUpdateProgram, + onSuccess: (updatedProgram: AdminProgramResponse): void => { + queryClient.setQueriesData( + { queryKey: adminQueries.programs.all() }, + (oldData) => { + if (!oldData) return oldData + return { + ...oldData, + items: oldData.items.map((program) => + program.id === updatedProgram.id ? updatedProgram : program + ), + } + } + ) + toast.success(ADMIN_SUCCESS_MESSAGES.PROGRAM_UPDATED) + }, + onError: (error: Error): void => { + const message = + error instanceof AdminResponseError + ? error.message + : ADMIN_ERROR_MESSAGES.FAILED_TO_UPDATE_PROGRAM + toast.error(message) + }, + }) +} + +const performAdminDeleteProgram = async (id: string): Promise => { + await apiClient.delete(API_ENDPOINTS.ADMIN.PROGRAMS.DELETE(id)) +} + +export const useAdminDeleteProgram = (): UseMutationResult< + void, + Error, + string +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: performAdminDeleteProgram, + onSuccess: (_, deletedId: string): void => { + queryClient.setQueriesData( + { queryKey: adminQueries.programs.all() }, + (oldData) => { + if (!oldData) return oldData + return { + ...oldData, + items: oldData.items.filter((program) => program.id !== deletedId), + total: oldData.total - 1, + } + } + ) + toast.success(ADMIN_SUCCESS_MESSAGES.PROGRAM_DELETED) + }, + onError: (error: Error): void => { + const message = + error instanceof AdminResponseError + ? error.message + : ADMIN_ERROR_MESSAGES.FAILED_TO_DELETE_PROGRAM + toast.error(message) + }, + }) +} + +interface UseAdminReportsParams { + page?: number + size?: number + status?: string + severity?: string +} + +const fetchAdminReports = async ( + page: number, + size: number, + status?: string, + severity?: string +): Promise => { + const params: Record = { page, size } + if (status) { + params.status = status + } + if (severity) { + params.severity = severity + } + + const response = await apiClient.get( + API_ENDPOINTS.ADMIN.REPORTS.LIST, + { + params, + } + ) + const data: unknown = response.data + + if (!isValidAdminReportListResponse(data)) { + throw new AdminResponseError( + ADMIN_ERROR_MESSAGES.INVALID_REPORT_LIST_RESPONSE, + API_ENDPOINTS.ADMIN.REPORTS.LIST + ) + } + + return data +} + +export const useAdminReports = ( + params: UseAdminReportsParams = {} +): UseQueryResult => { + const page = params.page ?? PAGINATION.DEFAULT_PAGE + const size = params.size ?? PAGINATION.DEFAULT_SIZE + + return useQuery({ + queryKey: adminQueries.reports.list( + page, + size, + params.status, + params.severity + ), + queryFn: () => fetchAdminReports(page, size, params.status, params.severity), + ...QUERY_STRATEGIES.standard, + }) +} + +interface AdminUpdateReportParams { + id: string + data: AdminReportUpdate +} + +const performAdminUpdateReport = async ( + params: AdminUpdateReportParams +): Promise => { + const response = await apiClient.patch( + API_ENDPOINTS.ADMIN.REPORTS.UPDATE(params.id), + params.data + ) + const responseData: unknown = response.data + + if (!isValidAdminReportResponse(responseData)) { + throw new AdminResponseError( + ADMIN_ERROR_MESSAGES.INVALID_REPORT_RESPONSE, + API_ENDPOINTS.ADMIN.REPORTS.UPDATE(params.id) + ) + } + + return responseData +} + +export const useAdminUpdateReport = (): UseMutationResult< + AdminReportResponse, + Error, + AdminUpdateReportParams +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: performAdminUpdateReport, + onSuccess: (updatedReport: AdminReportResponse): void => { + queryClient.setQueriesData( + { queryKey: adminQueries.reports.all() }, + (oldData) => { + if (!oldData) return oldData + return { + ...oldData, + items: oldData.items.map((report) => + report.id === updatedReport.id ? updatedReport : report + ), + } + } + ) + toast.success(ADMIN_SUCCESS_MESSAGES.REPORT_UPDATED) + }, + onError: (error: Error): void => { + const message = + error instanceof AdminResponseError + ? error.message + : ADMIN_ERROR_MESSAGES.FAILED_TO_UPDATE_REPORT + toast.error(message) + }, + }) +} + +interface UseAdminUsersWithStatsParams { + page?: number + size?: number + role?: string +} + +const fetchAdminUsersWithStats = async ( + page: number, + size: number, + role?: string +): Promise => { + const params: Record = { page, size } + if (role) { + params.role = role + } + + const response = await apiClient.get(API_ENDPOINTS.ADMIN.USERS.LIST, { + params, + }) + const data: unknown = response.data + + if (!isValidAdminUserListResponse(data)) { + throw new AdminResponseError( + ADMIN_ERROR_MESSAGES.INVALID_USER_LIST_RESPONSE, + API_ENDPOINTS.ADMIN.USERS.LIST + ) + } + + return data +} + +export const useAdminUsersWithStats = ( + params: UseAdminUsersWithStatsParams = {} +): UseQueryResult => { + const page = params.page ?? PAGINATION.DEFAULT_PAGE + const size = params.size ?? PAGINATION.DEFAULT_SIZE + + return useQuery({ + queryKey: adminQueries.users.list(page, size, params.role), + queryFn: () => fetchAdminUsersWithStats(page, size, params.role), + ...QUERY_STRATEGIES.standard, + }) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAuth.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAuth.ts new file mode 100644 index 00000000..a0aea655 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useAuth.ts @@ -0,0 +1,243 @@ +// =================== +// © AngelaMos | 2025 +// useAuth.ts +// =================== + +import { + type UseMutationResult, + type UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import { toast } from 'sonner' +import { + AUTH_ERROR_MESSAGES, + AUTH_SUCCESS_MESSAGES, + AuthResponseError, + isValidLogoutAllResponse, + isValidTokenResponse, + isValidTokenWithUserResponse, + isValidUserResponse, + type LoginRequest, + type LogoutAllResponse, + type PasswordChangeRequest, + type TokenWithUserResponse, + type UserResponse, +} from '@/api/types' +import { API_ENDPOINTS, QUERY_KEYS, ROUTES } from '@/config' +import { apiClient, QUERY_STRATEGIES } from '@/core/api' +import { useAuthStore } from '@/core/lib' + +export const authQueries = { + all: () => QUERY_KEYS.AUTH.ALL, + me: () => QUERY_KEYS.AUTH.ME(), +} as const + +const fetchCurrentUser = async (): Promise => { + const response = await apiClient.get(API_ENDPOINTS.AUTH.ME) + const data: unknown = response.data + + if (!isValidUserResponse(data)) { + throw new AuthResponseError( + AUTH_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.AUTH.ME + ) + } + + return data +} + +export const useCurrentUser = (): UseQueryResult => { + const isAuthenticated = useAuthStore((s) => s.isAuthenticated) + + return useQuery({ + queryKey: authQueries.me(), + queryFn: fetchCurrentUser, + enabled: isAuthenticated, + ...QUERY_STRATEGIES.auth, + }) +} + +const performLogin = async ( + credentials: LoginRequest +): Promise => { + const formData = new URLSearchParams() + formData.append('username', credentials.username) + formData.append('password', credentials.password) + + const response = await apiClient.post( + API_ENDPOINTS.AUTH.LOGIN, + formData, + { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + } + ) + + const data: unknown = response.data + + if (!isValidTokenWithUserResponse(data)) { + throw new AuthResponseError( + AUTH_ERROR_MESSAGES.INVALID_LOGIN_RESPONSE, + API_ENDPOINTS.AUTH.LOGIN + ) + } + + return data +} + +export const useLogin = (): UseMutationResult< + TokenWithUserResponse, + Error, + LoginRequest +> => { + const queryClient = useQueryClient() + const login = useAuthStore((s) => s.login) + + return useMutation({ + mutationFn: performLogin, + onSuccess: (data: TokenWithUserResponse): void => { + login(data.user, data.access_token) + + queryClient.setQueryData(authQueries.me(), data.user) + + const welcomeMessage = AUTH_SUCCESS_MESSAGES.WELCOME_BACK( + data.user.full_name + ) + toast.success(welcomeMessage) + }, + onError: (error: Error): void => { + const message = + error instanceof AuthResponseError ? error.message : 'Login failed' + toast.error(message) + }, + }) +} + +const performLogout = async (): Promise => { + await apiClient.post(API_ENDPOINTS.AUTH.LOGOUT) +} + +export const useLogout = (): UseMutationResult => { + const queryClient = useQueryClient() + const logout = useAuthStore((s) => s.logout) + + return useMutation({ + mutationFn: performLogout, + onSuccess: (): void => { + logout() + + queryClient.removeQueries({ queryKey: authQueries.all() }) + + toast.success(AUTH_SUCCESS_MESSAGES.LOGOUT_SUCCESS) + + window.location.href = ROUTES.LOGIN + }, + onError: (): void => { + logout() + queryClient.removeQueries({ queryKey: authQueries.all() }) + window.location.href = ROUTES.LOGIN + }, + }) +} + +const performLogoutAll = async (): Promise => { + const response = await apiClient.post(API_ENDPOINTS.AUTH.LOGOUT_ALL) + const data: unknown = response.data + + if (!isValidLogoutAllResponse(data)) { + throw new AuthResponseError( + AUTH_ERROR_MESSAGES.INVALID_LOGOUT_RESPONSE, + API_ENDPOINTS.AUTH.LOGOUT_ALL + ) + } + + return data +} + +export const useLogoutAll = (): UseMutationResult< + LogoutAllResponse, + Error, + void +> => { + const queryClient = useQueryClient() + const logout = useAuthStore((s) => s.logout) + + return useMutation({ + mutationFn: performLogoutAll, + onSuccess: (data: LogoutAllResponse): void => { + logout() + + queryClient.removeQueries({ queryKey: authQueries.all() }) + + toast.success(`Logged out from ${data.revoked_sessions} session(s)`) + + window.location.href = ROUTES.LOGIN + }, + onError: (error: Error): void => { + const message = + error instanceof AuthResponseError + ? error.message + : 'Failed to logout all sessions' + toast.error(message) + }, + }) +} + +const performPasswordChange = async ( + data: PasswordChangeRequest +): Promise => { + await apiClient.post(API_ENDPOINTS.AUTH.CHANGE_PASSWORD, data) +} + +export const useChangePassword = (): UseMutationResult< + void, + Error, + PasswordChangeRequest +> => { + return useMutation({ + mutationFn: performPasswordChange, + onSuccess: (): void => { + toast.success(AUTH_SUCCESS_MESSAGES.PASSWORD_CHANGED) + }, + onError: (error: Error): void => { + const message = + error instanceof AuthResponseError + ? error.message + : 'Failed to change password' + toast.error(message) + }, + }) +} + +export const useRefreshAuth = (): (() => Promise) => { + const queryClient = useQueryClient() + const { setAccessToken, login, logout } = useAuthStore() + + return async (): Promise => { + try { + const response = await apiClient.post(API_ENDPOINTS.AUTH.REFRESH) + const data: unknown = response.data + + if (!isValidTokenResponse(data)) { + throw new AuthResponseError( + AUTH_ERROR_MESSAGES.INVALID_TOKEN_RESPONSE, + API_ENDPOINTS.AUTH.REFRESH + ) + } + + setAccessToken(data.access_token) + + const userResponse = await apiClient.get(API_ENDPOINTS.AUTH.ME) + const userData: unknown = userResponse.data + + if (isValidUserResponse(userData)) { + login(userData, data.access_token) + queryClient.setQueryData(authQueries.me(), userData) + } + } catch { + logout() + queryClient.removeQueries({ queryKey: authQueries.all() }) + } + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/usePrograms.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/usePrograms.ts new file mode 100644 index 00000000..0e54a310 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/usePrograms.ts @@ -0,0 +1,337 @@ +// =================== +// AngelaMos | 2025 +// usePrograms.ts +// =================== + +import { + type UseMutationResult, + type UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import { toast } from 'sonner' + +import { + type Asset, + type AssetCreate, + isValidProgramDetail, + isValidProgramList, + type Program, + type ProgramCreate, + type ProgramDetail, + type ProgramList, + type ProgramUpdate, + type RewardTier, + type RewardTierCreate, +} from '@/api/types' +import { API_ENDPOINTS, QUERY_KEYS } from '@/config' +import { apiClient } from '@/core/api' + +export const programQueries = { + all: () => QUERY_KEYS.PROGRAMS.ALL, + list: (page: number, size: number) => QUERY_KEYS.PROGRAMS.LIST(page, size), + mine: (page: number, size: number) => QUERY_KEYS.PROGRAMS.MINE(page, size), + bySlug: (slug: string) => QUERY_KEYS.PROGRAMS.BY_SLUG(slug), +} as const + +const fetchPrograms = async ( + page: number, + size: number +): Promise => { + const response = await apiClient.get(API_ENDPOINTS.PROGRAMS.LIST, { + params: { page, size }, + }) + + if (!isValidProgramList(response.data)) { + throw new Error('Invalid program list response') + } + + return response.data +} + +export const usePrograms = ( + page: number = 1, + size: number = 20 +): UseQueryResult => { + return useQuery({ + queryKey: programQueries.list(page, size), + queryFn: () => fetchPrograms(page, size), + }) +} + +const fetchMyPrograms = async ( + page: number, + size: number +): Promise => { + const response = await apiClient.get(API_ENDPOINTS.PROGRAMS.MINE, { + params: { page, size }, + }) + + if (!isValidProgramList(response.data)) { + throw new Error('Invalid program list response') + } + + return response.data +} + +export const useMyPrograms = ( + page: number = 1, + size: number = 20 +): UseQueryResult => { + return useQuery({ + queryKey: programQueries.mine(page, size), + queryFn: () => fetchMyPrograms(page, size), + }) +} + +const fetchProgramBySlug = async (slug: string): Promise => { + const response = await apiClient.get( + API_ENDPOINTS.PROGRAMS.BY_SLUG(slug) + ) + + if (!isValidProgramDetail(response.data)) { + throw new Error('Invalid program detail response') + } + + return response.data +} + +export const useProgram = ( + slug: string +): UseQueryResult => { + return useQuery({ + queryKey: programQueries.bySlug(slug), + queryFn: () => fetchProgramBySlug(slug), + enabled: !!slug, + }) +} + +const createProgram = async (data: ProgramCreate): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.PROGRAMS.CREATE, + data + ) + return response.data +} + +export const useCreateProgram = (): UseMutationResult< + Program, + Error, + ProgramCreate +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: createProgram, + onSuccess: (newProgram) => { + queryClient.invalidateQueries({ queryKey: programQueries.list(1, 20) }) + queryClient.invalidateQueries({ queryKey: programQueries.mine(1, 20) }) + + toast.success(`Program "${newProgram.name}" created successfully`) + }, + onError: () => { + toast.error('Failed to create program') + }, + }) +} + +const updateProgram = async ({ + id, + data, +}: { + id: string + data: ProgramUpdate +}): Promise => { + const response = await apiClient.patch( + API_ENDPOINTS.PROGRAMS.BY_ID(id), + data + ) + return response.data +} + +export const useUpdateProgram = (): UseMutationResult< + Program, + Error, + { id: string; data: ProgramUpdate } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: updateProgram, + onSuccess: (updatedProgram) => { + const queries = queryClient.getQueriesData({ + queryKey: programQueries.all(), + }) + + for (const [key, data] of queries) { + if (data && data.id === updatedProgram.id) { + queryClient.setQueryData(key, { + ...data, + ...updatedProgram, + }) + } + } + + queryClient.invalidateQueries({ queryKey: programQueries.list(1, 20) }) + queryClient.invalidateQueries({ queryKey: programQueries.mine(1, 20) }) + + toast.success(`Program "${updatedProgram.name}" updated successfully`) + }, + onError: () => { + toast.error('Failed to update program') + }, + }) +} + +const deleteProgram = async (id: string): Promise => { + await apiClient.delete(API_ENDPOINTS.PROGRAMS.BY_ID(id)) +} + +export const useDeleteProgram = (): UseMutationResult => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: deleteProgram, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: programQueries.list(1, 20) }) + queryClient.invalidateQueries({ queryKey: programQueries.mine(1, 20) }) + + toast.success('Program deleted successfully') + }, + onError: () => { + toast.error('Failed to delete program') + }, + }) +} + +const addAsset = async ({ + programId, + data, +}: { + programId: string + data: AssetCreate +}): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.PROGRAMS.ASSETS(programId), + data + ) + return response.data +} + +export const useAddAsset = (): UseMutationResult< + Asset, + Error, + { programId: string; data: AssetCreate } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: addAsset, + onSuccess: (newAsset, variables) => { + const queries = queryClient.getQueriesData({ + queryKey: programQueries.all(), + }) + + for (const [key, data] of queries) { + if (data && 'assets' in data && data.id === variables.programId) { + queryClient.setQueryData(key, { + ...data, + assets: [...data.assets, newAsset], + }) + } + } + + toast.success('Asset added successfully') + }, + onError: () => { + toast.error('Failed to add asset') + }, + }) +} + +const deleteAsset = async ({ + programId, + assetId, +}: { + programId: string + assetId: string +}): Promise => { + await apiClient.delete(API_ENDPOINTS.PROGRAMS.ASSET(programId, assetId)) +} + +export const useDeleteAsset = (): UseMutationResult< + void, + Error, + { programId: string; assetId: string } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: deleteAsset, + onSuccess: (_, variables) => { + const queries = queryClient.getQueriesData({ + queryKey: programQueries.all(), + }) + + for (const [key, data] of queries) { + if (data && 'assets' in data && data.id === variables.programId) { + queryClient.setQueryData(key, { + ...data, + assets: data.assets.filter((a) => a.id !== variables.assetId), + }) + } + } + + toast.success('Asset deleted successfully') + }, + onError: () => { + toast.error('Failed to delete asset') + }, + }) +} + +const setRewardTiers = async ({ + programId, + tiers, +}: { + programId: string + tiers: RewardTierCreate[] +}): Promise => { + const response = await apiClient.put( + API_ENDPOINTS.PROGRAMS.REWARDS(programId), + tiers + ) + return response.data +} + +export const useSetRewardTiers = (): UseMutationResult< + RewardTier[], + Error, + { programId: string; tiers: RewardTierCreate[] } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: setRewardTiers, + onSuccess: (updatedTiers, variables) => { + const queries = queryClient.getQueriesData({ + queryKey: programQueries.all(), + }) + + for (const [key, data] of queries) { + if (data && 'reward_tiers' in data && data.id === variables.programId) { + queryClient.setQueryData(key, { + ...data, + reward_tiers: updatedTiers, + }) + } + } + + toast.success('Reward tiers updated successfully') + }, + onError: () => { + toast.error('Failed to update reward tiers') + }, + }) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useReports.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useReports.ts new file mode 100644 index 00000000..406a6949 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useReports.ts @@ -0,0 +1,323 @@ +// =================== +// AngelaMos | 2025 +// useReports.ts +// =================== + +import { + type UseMutationResult, + type UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import { toast } from 'sonner' + +import { + type Comment, + type CommentCreate, + isValidReportDetail, + isValidReportList, + isValidReportStats, + type Report, + type ReportCreate, + type ReportDetail, + type ReportList, + type ReportStats, + type ReportTriage, + type ReportUpdate, +} from '@/api/types' +import { API_ENDPOINTS, QUERY_KEYS } from '@/config' +import { apiClient } from '@/core/api' + +export const reportQueries = { + all: () => QUERY_KEYS.REPORTS.ALL, + list: (page: number, size: number) => QUERY_KEYS.REPORTS.LIST(page, size), + inbox: (page: number, size: number) => QUERY_KEYS.REPORTS.INBOX(page, size), + stats: () => QUERY_KEYS.REPORTS.STATS(), + byId: (id: string) => QUERY_KEYS.REPORTS.BY_ID(id), + byProgram: (programId: string, page: number, size: number) => + QUERY_KEYS.REPORTS.BY_PROGRAM(programId, page, size), +} as const + +const fetchMyReports = async ( + page: number, + size: number +): Promise => { + const response = await apiClient.get(API_ENDPOINTS.REPORTS.LIST, { + params: { page, size }, + }) + + if (!isValidReportList(response.data)) { + throw new Error('Invalid report list response') + } + + return response.data +} + +export const useMyReports = ( + page: number = 1, + size: number = 20 +): UseQueryResult => { + return useQuery({ + queryKey: reportQueries.list(page, size), + queryFn: () => fetchMyReports(page, size), + }) +} + +const fetchInbox = async (page: number, size: number): Promise => { + const response = await apiClient.get(API_ENDPOINTS.REPORTS.INBOX, { + params: { page, size }, + }) + + if (!isValidReportList(response.data)) { + throw new Error('Invalid report list response') + } + + return response.data +} + +export const useInbox = ( + page: number = 1, + size: number = 20 +): UseQueryResult => { + return useQuery({ + queryKey: reportQueries.inbox(page, size), + queryFn: () => fetchInbox(page, size), + }) +} + +const fetchReportStats = async (): Promise => { + const response = await apiClient.get(API_ENDPOINTS.REPORTS.STATS) + + if (!isValidReportStats(response.data)) { + throw new Error('Invalid report stats response') + } + + return response.data +} + +export const useReportStats = (): UseQueryResult => { + return useQuery({ + queryKey: reportQueries.stats(), + queryFn: fetchReportStats, + }) +} + +const fetchReport = async (id: string): Promise => { + const response = await apiClient.get(API_ENDPOINTS.REPORTS.BY_ID(id)) + + if (!isValidReportDetail(response.data)) { + throw new Error('Invalid report detail response') + } + + return response.data +} + +export const useReport = (id: string): UseQueryResult => { + return useQuery({ + queryKey: reportQueries.byId(id), + queryFn: () => fetchReport(id), + enabled: !!id, + }) +} + +const fetchProgramReports = async ( + programId: string, + page: number, + size: number +): Promise => { + const response = await apiClient.get( + API_ENDPOINTS.REPORTS.BY_PROGRAM(programId), + { + params: { page, size }, + } + ) + + if (!isValidReportList(response.data)) { + throw new Error('Invalid report list response') + } + + return response.data +} + +export const useProgramReports = ( + programId: string, + page: number = 1, + size: number = 20 +): UseQueryResult => { + return useQuery({ + queryKey: reportQueries.byProgram(programId, page, size), + queryFn: () => fetchProgramReports(programId, page, size), + enabled: !!programId, + }) +} + +const submitReport = async (data: ReportCreate): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.REPORTS.SUBMIT, + data + ) + return response.data +} + +export const useSubmitReport = (): UseMutationResult< + Report, + Error, + ReportCreate +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: submitReport, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: reportQueries.list(1, 20) }) + queryClient.invalidateQueries({ queryKey: reportQueries.inbox(1, 20) }) + queryClient.invalidateQueries({ queryKey: reportQueries.stats() }) + + toast.success('Report submitted successfully') + }, + onError: () => { + toast.error('Failed to submit report') + }, + }) +} + +const updateReport = async ({ + id, + data, +}: { + id: string + data: ReportUpdate +}): Promise => { + const response = await apiClient.patch( + API_ENDPOINTS.REPORTS.BY_ID(id), + data + ) + return response.data +} + +export const useUpdateReport = (): UseMutationResult< + Report, + Error, + { id: string; data: ReportUpdate } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: updateReport, + onSuccess: (updatedReport) => { + const queries = queryClient.getQueriesData({ + queryKey: reportQueries.all(), + }) + + for (const [key, data] of queries) { + if (data && 'comments' in data && data.id === updatedReport.id) { + queryClient.setQueryData(key, { + ...data, + ...updatedReport, + }) + } + } + + queryClient.invalidateQueries({ queryKey: reportQueries.list(1, 20) }) + queryClient.invalidateQueries({ queryKey: reportQueries.inbox(1, 20) }) + + toast.success('Report updated successfully') + }, + onError: () => { + toast.error('Failed to update report') + }, + }) +} + +const triageReport = async ({ + id, + data, +}: { + id: string + data: ReportTriage +}): Promise => { + const response = await apiClient.patch( + API_ENDPOINTS.REPORTS.TRIAGE(id), + data + ) + return response.data +} + +export const useTriageReport = (): UseMutationResult< + Report, + Error, + { id: string; data: ReportTriage } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: triageReport, + onSuccess: (updatedReport) => { + const queries = queryClient.getQueriesData({ + queryKey: reportQueries.all(), + }) + + for (const [key, data] of queries) { + if (data && 'comments' in data && data.id === updatedReport.id) { + queryClient.setQueryData(key, { + ...data, + ...updatedReport, + }) + } + } + + queryClient.invalidateQueries({ queryKey: reportQueries.list(1, 20) }) + queryClient.invalidateQueries({ queryKey: reportQueries.inbox(1, 20) }) + queryClient.invalidateQueries({ queryKey: reportQueries.stats() }) + + toast.success('Report triaged successfully') + }, + onError: () => { + toast.error('Failed to triage report') + }, + }) +} + +const addComment = async ({ + reportId, + data, +}: { + reportId: string + data: CommentCreate +}): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.REPORTS.COMMENTS(reportId), + data + ) + return response.data +} + +export const useAddComment = (): UseMutationResult< + Comment, + Error, + { reportId: string; data: CommentCreate } +> => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: addComment, + onSuccess: (newComment, variables) => { + queryClient.setQueryData( + reportQueries.byId(variables.reportId), + (old) => { + if (!old || !('comments' in old)) return old + return { + ...old, + comments: [...old.comments, newComment], + } + } + ) + + toast.success('Comment added successfully') + }, + onError: () => { + toast.error('Failed to add comment') + }, + }) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useUsers.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useUsers.ts new file mode 100644 index 00000000..ba3a2264 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/hooks/useUsers.ts @@ -0,0 +1,138 @@ +// =================== +// © AngelaMos | 2025 +// useUsers.ts +// =================== + +import { + type UseMutationResult, + type UseQueryResult, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' +import { toast } from 'sonner' +import { + isValidUserResponse, + USER_ERROR_MESSAGES, + USER_SUCCESS_MESSAGES, + type UserCreateRequest, + type UserResponse, + UserResponseError, + type UserUpdateRequest, +} from '@/api/types' +import { API_ENDPOINTS, QUERY_KEYS } from '@/config' +import { apiClient, QUERY_STRATEGIES } from '@/core/api' +import { useAuthStore } from '@/core/lib' +import { authQueries } from './useAuth' + +export const userQueries = { + all: () => QUERY_KEYS.USERS.ALL, + byId: (id: string) => QUERY_KEYS.USERS.BY_ID(id), + me: () => QUERY_KEYS.USERS.ME(), +} as const + +const fetchUserById = async (id: string): Promise => { + const response = await apiClient.get(API_ENDPOINTS.USERS.BY_ID(id)) + const data: unknown = response.data + + if (!isValidUserResponse(data)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.USERS.BY_ID(id) + ) + } + + return data +} + +export const useUser = (id: string): UseQueryResult => { + return useQuery({ + queryKey: userQueries.byId(id), + queryFn: () => fetchUserById(id), + enabled: id.length > 0, + ...QUERY_STRATEGIES.standard, + }) +} + +const performRegister = async ( + data: UserCreateRequest +): Promise => { + const response = await apiClient.post( + API_ENDPOINTS.USERS.REGISTER, + data + ) + const responseData: unknown = response.data + + if (!isValidUserResponse(responseData)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.USERS.REGISTER + ) + } + + return responseData +} + +export const useRegister = (): UseMutationResult< + UserResponse, + Error, + UserCreateRequest +> => { + return useMutation({ + mutationFn: performRegister, + onSuccess: (): void => { + toast.success(USER_SUCCESS_MESSAGES.REGISTERED) + }, + onError: (error: Error): void => { + const message = + error instanceof UserResponseError + ? error.message + : USER_ERROR_MESSAGES.FAILED_TO_CREATE + toast.error(message) + }, + }) +} + +const performUpdateProfile = async ( + data: UserUpdateRequest +): Promise => { + const response = await apiClient.patch(API_ENDPOINTS.USERS.ME, data) + const responseData: unknown = response.data + + if (!isValidUserResponse(responseData)) { + throw new UserResponseError( + USER_ERROR_MESSAGES.INVALID_USER_RESPONSE, + API_ENDPOINTS.USERS.ME + ) + } + + return responseData +} + +export const useUpdateProfile = (): UseMutationResult< + UserResponse, + Error, + UserUpdateRequest +> => { + const queryClient = useQueryClient() + const updateUser = useAuthStore((s) => s.updateUser) + + return useMutation({ + mutationFn: performUpdateProfile, + onSuccess: (data: UserResponse): void => { + updateUser(data) + + queryClient.setQueryData(authQueries.me(), data) + queryClient.setQueryData(userQueries.me(), data) + + toast.success(USER_SUCCESS_MESSAGES.PROFILE_UPDATED) + }, + onError: (error: Error): void => { + const message = + error instanceof UserResponseError + ? error.message + : USER_ERROR_MESSAGES.FAILED_TO_UPDATE + toast.error(message) + }, + }) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/index.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/index.ts new file mode 100644 index 00000000..0840a7c1 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/index.ts @@ -0,0 +1,7 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './hooks' +export * from './types' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/types/admin.types.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/types/admin.types.ts new file mode 100644 index 00000000..505fddf3 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/types/admin.types.ts @@ -0,0 +1,187 @@ +// =================== +// AngelaMos | 2026 +// admin.types.ts +// =================== + +import { z } from 'zod' +import { UserRole } from './auth.types' +import { ProgramStatus, ProgramVisibility, Severity } from './program.types' +import { ReportStatus } from './report.types' + +export const adminProgramResponseSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + company_id: z.string().uuid(), + company_email: z.string(), + company_name: z.string().nullable(), + name: z.string(), + slug: z.string(), + description: z.string().nullable(), + status: z.nativeEnum(ProgramStatus), + visibility: z.nativeEnum(ProgramVisibility), + response_sla_hours: z.number(), + report_count: z.number(), +}) + +export const adminProgramListResponseSchema = z.object({ + items: z.array(adminProgramResponseSchema), + total: z.number(), + page: z.number(), + size: z.number(), +}) + +export const adminProgramUpdateSchema = z.object({ + name: z.string().optional(), + status: z.nativeEnum(ProgramStatus).optional(), + visibility: z.nativeEnum(ProgramVisibility).optional(), + is_featured: z.boolean().optional(), +}) + +export const adminReportResponseSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + program_id: z.string().uuid(), + program_name: z.string(), + program_slug: z.string(), + researcher_id: z.string().uuid(), + researcher_email: z.string(), + researcher_name: z.string().nullable(), + title: z.string(), + severity_submitted: z.nativeEnum(Severity), + severity_final: z.nativeEnum(Severity).nullable(), + status: z.nativeEnum(ReportStatus), + bounty_amount: z.number().nullable(), + triaged_at: z.string().datetime().nullable(), + resolved_at: z.string().datetime().nullable(), +}) + +export const adminReportListResponseSchema = z.object({ + items: z.array(adminReportResponseSchema), + total: z.number(), + page: z.number(), + size: z.number(), +}) + +export const adminReportUpdateSchema = z.object({ + status: z.nativeEnum(ReportStatus).optional(), + severity_final: z.nativeEnum(Severity).optional(), + cvss_score: z.number().min(0).max(10).optional(), + bounty_amount: z.number().min(0).optional(), + admin_notes: z.string().optional(), +}) + +export const platformStatsResponseSchema = z.object({ + total_users: z.number(), + total_researchers: z.number(), + total_companies: z.number(), + total_programs: z.number(), + active_programs: z.number(), + total_reports: z.number(), + reports_by_status: z.record(z.string(), z.number()), + total_bounties_paid: z.number(), + reports_this_month: z.number(), + new_users_this_month: z.number(), +}) + +export const adminUserResponseSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + email: z.string(), + full_name: z.string().nullable(), + company_name: z.string().nullable(), + is_active: z.boolean(), + is_verified: z.boolean(), + role: z.nativeEnum(UserRole), + reputation_score: z.number(), + program_count: z.number(), + report_count: z.number(), +}) + +export const adminUserListResponseSchema = z.object({ + items: z.array(adminUserResponseSchema), + total: z.number(), + page: z.number(), + size: z.number(), +}) + +export type AdminProgramResponse = z.infer +export type AdminProgramListResponse = z.infer< + typeof adminProgramListResponseSchema +> +export type AdminProgramUpdate = z.infer +export type AdminReportResponse = z.infer +export type AdminReportListResponse = z.infer< + typeof adminReportListResponseSchema +> +export type AdminReportUpdate = z.infer +export type PlatformStatsResponse = z.infer +export type AdminUserResponse = z.infer +export type AdminUserListResponse = z.infer + +export const isValidAdminProgramListResponse = ( + data: unknown +): data is AdminProgramListResponse => { + return adminProgramListResponseSchema.safeParse(data).success +} + +export const isValidAdminReportListResponse = ( + data: unknown +): data is AdminReportListResponse => { + return adminReportListResponseSchema.safeParse(data).success +} + +export const isValidPlatformStatsResponse = ( + data: unknown +): data is PlatformStatsResponse => { + return platformStatsResponseSchema.safeParse(data).success +} + +export const isValidAdminUserListResponse = ( + data: unknown +): data is AdminUserListResponse => { + return adminUserListResponseSchema.safeParse(data).success +} + +export const isValidAdminProgramResponse = ( + data: unknown +): data is AdminProgramResponse => { + return adminProgramResponseSchema.safeParse(data).success +} + +export const isValidAdminReportResponse = ( + data: unknown +): data is AdminReportResponse => { + return adminReportResponseSchema.safeParse(data).success +} + +export class AdminResponseError extends Error { + readonly endpoint?: string + + constructor(message: string, endpoint?: string) { + super(message) + this.name = 'AdminResponseError' + this.endpoint = endpoint + Object.setPrototypeOf(this, AdminResponseError.prototype) + } +} + +export const ADMIN_ERROR_MESSAGES = { + INVALID_PROGRAM_LIST_RESPONSE: 'Invalid admin program list from server', + INVALID_PROGRAM_RESPONSE: 'Invalid admin program data from server', + INVALID_REPORT_LIST_RESPONSE: 'Invalid admin report list from server', + INVALID_REPORT_RESPONSE: 'Invalid admin report data from server', + INVALID_STATS_RESPONSE: 'Invalid platform stats from server', + INVALID_USER_LIST_RESPONSE: 'Invalid admin user list from server', + FAILED_TO_UPDATE_PROGRAM: 'Failed to update program', + FAILED_TO_DELETE_PROGRAM: 'Failed to delete program', + FAILED_TO_UPDATE_REPORT: 'Failed to update report', +} as const + +export const ADMIN_SUCCESS_MESSAGES = { + PROGRAM_UPDATED: 'Program updated successfully', + PROGRAM_DELETED: 'Program deleted successfully', + REPORT_UPDATED: 'Report updated successfully', +} as const diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/types/auth.types.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/types/auth.types.ts new file mode 100644 index 00000000..bf1ff764 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/types/auth.types.ts @@ -0,0 +1,140 @@ +// =================== +// © AngelaMos | 2025 +// auth.types.ts +// =================== + +import { z } from 'zod' +import { PASSWORD_CONSTRAINTS } from '@/config' + +export const UserRole = { + UNKNOWN: 'unknown', + USER: 'user', + ADMIN: 'admin', +} as const + +export type UserRole = (typeof UserRole)[keyof typeof UserRole] + +export const userResponseSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + email: z.string().email(), + full_name: z.string().nullable(), + is_active: z.boolean(), + is_verified: z.boolean(), + role: z.nativeEnum(UserRole), +}) + +export const tokenResponseSchema = z.object({ + access_token: z.string(), + token_type: z.string(), +}) + +export const tokenWithUserResponseSchema = tokenResponseSchema.extend({ + user: userResponseSchema, +}) + +export const loginRequestSchema = z.object({ + username: z.string().email(), + password: z + .string() + .min(PASSWORD_CONSTRAINTS.MIN_LENGTH) + .max(PASSWORD_CONSTRAINTS.MAX_LENGTH), +}) + +export const registerRequestSchema = z.object({ + email: z.string().email(), + password: z + .string() + .min(PASSWORD_CONSTRAINTS.MIN_LENGTH) + .max(PASSWORD_CONSTRAINTS.MAX_LENGTH), + full_name: z.string().max(255).optional(), +}) + +export const passwordChangeRequestSchema = z.object({ + current_password: z.string(), + new_password: z + .string() + .min(PASSWORD_CONSTRAINTS.MIN_LENGTH) + .max(PASSWORD_CONSTRAINTS.MAX_LENGTH), +}) + +export const logoutAllResponseSchema = z.object({ + revoked_sessions: z.number(), +}) + +export type UserResponse = z.infer +export type TokenResponse = z.infer +export type TokenWithUserResponse = z.infer +export type LoginRequest = z.infer +export type RegisterRequest = z.infer +export type PasswordChangeRequest = z.infer +export type LogoutAllResponse = z.infer + +export const isValidUserResponse = (data: unknown): data is UserResponse => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = userResponseSchema.safeParse(data) + return result.success +} + +export const isValidTokenResponse = (data: unknown): data is TokenResponse => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = tokenResponseSchema.safeParse(data) + return result.success +} + +export const isValidTokenWithUserResponse = ( + data: unknown +): data is TokenWithUserResponse => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = tokenWithUserResponseSchema.safeParse(data) + return result.success +} + +export const isValidLogoutAllResponse = ( + data: unknown +): data is LogoutAllResponse => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = logoutAllResponseSchema.safeParse(data) + return result.success +} + +export class AuthResponseError extends Error { + readonly endpoint?: string + + constructor(message: string, endpoint?: string) { + super(message) + this.name = 'AuthResponseError' + this.endpoint = endpoint + Object.setPrototypeOf(this, AuthResponseError.prototype) + } +} + +export const AUTH_ERROR_MESSAGES = { + INVALID_USER_RESPONSE: 'Invalid user data from server', + INVALID_LOGIN_RESPONSE: 'Invalid login response from server', + INVALID_TOKEN_RESPONSE: 'Invalid token response from server', + INVALID_LOGOUT_RESPONSE: 'Invalid logout response from server', + NO_REFRESH_TOKEN: 'No refresh token available', + SESSION_EXPIRED: 'Session expired', +} as const + +export const AUTH_SUCCESS_MESSAGES = { + WELCOME_BACK: (name: string | null) => + `Welcome back${name !== null ? `, ${name}` : ''}!`, + LOGOUT_SUCCESS: 'Logged out successfully', + PASSWORD_CHANGED: 'Password changed successfully', + REGISTERED: 'Account created successfully!', +} as const + +export type AuthErrorMessage = + (typeof AUTH_ERROR_MESSAGES)[keyof typeof AUTH_ERROR_MESSAGES] +export type AuthSuccessMessage = typeof AUTH_SUCCESS_MESSAGES diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/types/index.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/types/index.ts new file mode 100644 index 00000000..f814525e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/types/index.ts @@ -0,0 +1,10 @@ +// =================== +// AngelaMos | 2026 +// index.ts +// =================== + +export * from './admin.types' +export * from './auth.types' +export * from './program.types' +export * from './report.types' +export * from './user.types' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/types/program.types.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/types/program.types.ts new file mode 100644 index 00000000..272f12e4 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/types/program.types.ts @@ -0,0 +1,183 @@ +// =================== +// AngelaMos | 2025 +// program.types.ts +// =================== + +import { z } from 'zod' + +export const ProgramStatus = { + DRAFT: 'draft', + ACTIVE: 'active', + PAUSED: 'paused', + CLOSED: 'closed', +} as const + +export type ProgramStatus = (typeof ProgramStatus)[keyof typeof ProgramStatus] + +export const ProgramVisibility = { + PUBLIC: 'public', + PRIVATE: 'private', + INVITE_ONLY: 'invite_only', +} as const + +export type ProgramVisibility = + (typeof ProgramVisibility)[keyof typeof ProgramVisibility] + +export const AssetType = { + DOMAIN: 'domain', + API: 'api', + MOBILE_APP: 'mobile_app', + SOURCE_CODE: 'source_code', + HARDWARE: 'hardware', + OTHER: 'other', +} as const + +export type AssetType = (typeof AssetType)[keyof typeof AssetType] + +export const Severity = { + CRITICAL: 'critical', + HIGH: 'high', + MEDIUM: 'medium', + LOW: 'low', + INFORMATIONAL: 'informational', +} as const + +export type Severity = (typeof Severity)[keyof typeof Severity] + +export const rewardTierSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + program_id: z.string().uuid(), + severity: z.nativeEnum(Severity), + min_bounty: z.number(), + max_bounty: z.number(), + currency: z.string(), +}) + +export const assetSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + program_id: z.string().uuid(), + asset_type: z.nativeEnum(AssetType), + identifier: z.string(), + in_scope: z.boolean(), + description: z.string().nullable(), +}) + +export const programSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + company_id: z.string().uuid(), + name: z.string(), + slug: z.string(), + description: z.string().nullable(), + rules: z.string().nullable(), + response_sla_hours: z.number(), + status: z.nativeEnum(ProgramStatus), + visibility: z.nativeEnum(ProgramVisibility), +}) + +export const programDetailSchema = programSchema.extend({ + assets: z.array(assetSchema), + reward_tiers: z.array(rewardTierSchema), +}) + +export const programListSchema = z.object({ + items: z.array(programSchema), + total: z.number(), + page: z.number(), + size: z.number(), +}) + +export const programCreateSchema = z.object({ + name: z.string().min(1).max(255), + slug: z + .string() + .min(1) + .max(100) + .regex(/^[a-z0-9-]+$/), + description: z.string().max(10000).optional(), + rules: z.string().max(50000).optional(), + response_sla_hours: z.number().min(1).max(720).default(72), + visibility: z.nativeEnum(ProgramVisibility).default(ProgramVisibility.PUBLIC), +}) + +export const programUpdateSchema = z.object({ + name: z.string().min(1).max(255).optional(), + description: z.string().max(10000).optional(), + rules: z.string().max(50000).optional(), + response_sla_hours: z.number().min(1).max(720).optional(), + status: z.nativeEnum(ProgramStatus).optional(), + visibility: z.nativeEnum(ProgramVisibility).optional(), +}) + +export const assetCreateSchema = z.object({ + asset_type: z.nativeEnum(AssetType).default(AssetType.DOMAIN), + identifier: z.string().min(1).max(500), + in_scope: z.boolean().default(true), + description: z.string().max(2000).optional(), +}) + +export const rewardTierCreateSchema = z.object({ + severity: z.nativeEnum(Severity), + min_bounty: z.number().min(0).default(0), + max_bounty: z.number().min(0).default(0), + currency: z.string().max(3).default('USD'), +}) + +export type RewardTier = z.infer +export type Asset = z.infer +export type Program = z.infer +export type ProgramDetail = z.infer +export type ProgramList = z.infer +export type ProgramCreate = z.infer +export type ProgramUpdate = z.infer +export type AssetCreate = z.infer +export type RewardTierCreate = z.infer + +export const isValidProgram = (data: unknown): data is Program => { + return programSchema.safeParse(data).success +} + +export const isValidProgramDetail = (data: unknown): data is ProgramDetail => { + return programDetailSchema.safeParse(data).success +} + +export const isValidProgramList = (data: unknown): data is ProgramList => { + return programListSchema.safeParse(data).success +} + +export const SEVERITY_LABELS: Record = { + critical: 'Critical', + high: 'High', + medium: 'Medium', + low: 'Low', + informational: 'Informational', +} + +export const SEVERITY_COLORS: Record = { + critical: '#dc2626', + high: '#ea580c', + medium: '#ca8a04', + low: '#2563eb', + informational: '#6b7280', +} + +export const STATUS_LABELS: Record = { + draft: 'Draft', + active: 'Active', + paused: 'Paused', + closed: 'Closed', +} + +export const ASSET_TYPE_LABELS: Record = { + domain: 'Domain', + api: 'API', + mobile_app: 'Mobile App', + source_code: 'Source Code', + hardware: 'Hardware', + other: 'Other', +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/types/report.types.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/types/report.types.ts new file mode 100644 index 00000000..1fe09922 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/types/report.types.ts @@ -0,0 +1,177 @@ +// =================== +// AngelaMos | 2025 +// report.types.ts +// =================== + +import { z } from 'zod' +import { Severity } from './program.types' + +export const ReportStatus = { + NEW: 'new', + TRIAGING: 'triaging', + NEEDS_MORE_INFO: 'needs_more_info', + ACCEPTED: 'accepted', + DUPLICATE: 'duplicate', + INFORMATIVE: 'informative', + NOT_APPLICABLE: 'not_applicable', + RESOLVED: 'resolved', + DISCLOSED: 'disclosed', +} as const + +export type ReportStatus = (typeof ReportStatus)[keyof typeof ReportStatus] + +export const commentSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + report_id: z.string().uuid(), + author_id: z.string().uuid(), + content: z.string(), + is_internal: z.boolean(), +}) + +export const attachmentSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + report_id: z.string().uuid(), + comment_id: z.string().uuid().nullable(), + filename: z.string(), + mime_type: z.string(), + size_bytes: z.number(), +}) + +export const reportSchema = z.object({ + id: z.string().uuid(), + created_at: z.string().datetime(), + updated_at: z.string().datetime().nullable(), + program_id: z.string().uuid(), + researcher_id: z.string().uuid(), + title: z.string(), + description: z.string(), + steps_to_reproduce: z.string().nullable(), + impact: z.string().nullable(), + severity_submitted: z.nativeEnum(Severity), + severity_final: z.nativeEnum(Severity).nullable(), + status: z.nativeEnum(ReportStatus), + cvss_score: z.number().nullable(), + cwe_id: z.string().nullable(), + bounty_amount: z.number().nullable(), + duplicate_of_id: z.string().uuid().nullable(), + triaged_at: z.string().datetime().nullable(), + resolved_at: z.string().datetime().nullable(), + disclosed_at: z.string().datetime().nullable(), +}) + +export const reportDetailSchema = reportSchema.extend({ + comments: z.array(commentSchema), + attachments: z.array(attachmentSchema), +}) + +export const reportListSchema = z.object({ + items: z.array(reportSchema), + total: z.number(), + page: z.number(), + size: z.number(), +}) + +export const reportStatsSchema = z.object({ + total_reports: z.number(), + accepted_reports: z.number(), + total_earned: z.number(), + reputation_score: z.number(), +}) + +export const reportCreateSchema = z.object({ + program_id: z.string().uuid(), + title: z.string().min(1).max(500), + description: z.string().min(1), + steps_to_reproduce: z.string().optional(), + impact: z.string().optional(), + severity_submitted: z.nativeEnum(Severity).default(Severity.MEDIUM), +}) + +export const reportUpdateSchema = z.object({ + title: z.string().min(1).max(500).optional(), + description: z.string().min(1).optional(), + steps_to_reproduce: z.string().optional(), + impact: z.string().optional(), + severity_submitted: z.nativeEnum(Severity).optional(), +}) + +export const reportTriageSchema = z.object({ + status: z.nativeEnum(ReportStatus).optional(), + severity_final: z.nativeEnum(Severity).optional(), + cvss_score: z.number().min(0).max(10).optional(), + cwe_id: z.string().max(20).optional(), + bounty_amount: z.number().min(0).optional(), + duplicate_of_id: z.string().uuid().optional(), +}) + +export const commentCreateSchema = z.object({ + content: z.string().min(1), + is_internal: z.boolean().default(false), +}) + +export type Comment = z.infer +export type Attachment = z.infer +export type Report = z.infer +export type ReportDetail = z.infer +export type ReportList = z.infer +export type ReportStats = z.infer +export type ReportCreate = z.infer +export type ReportUpdate = z.infer +export type ReportTriage = z.infer +export type CommentCreate = z.infer + +export const isValidReport = (data: unknown): data is Report => { + return reportSchema.safeParse(data).success +} + +export const isValidReportDetail = (data: unknown): data is ReportDetail => { + return reportDetailSchema.safeParse(data).success +} + +export const isValidReportList = (data: unknown): data is ReportList => { + return reportListSchema.safeParse(data).success +} + +export const isValidReportStats = (data: unknown): data is ReportStats => { + return reportStatsSchema.safeParse(data).success +} + +export const REPORT_STATUS_LABELS: Record = { + new: 'New', + triaging: 'Triaging', + needs_more_info: 'Needs More Info', + accepted: 'Accepted', + duplicate: 'Duplicate', + informative: 'Informative', + not_applicable: 'N/A', + resolved: 'Resolved', + disclosed: 'Disclosed', +} + +export const REPORT_STATUS_COLORS: Record = { + new: '#3b82f6', + triaging: '#f59e0b', + needs_more_info: '#8b5cf6', + accepted: '#22c55e', + duplicate: '#6b7280', + informative: '#06b6d4', + not_applicable: '#6b7280', + resolved: '#22c55e', + disclosed: '#10b981', +} + +export const isOpenStatus = (status: ReportStatus): boolean => { + return ([ + ReportStatus.NEW, + ReportStatus.TRIAGING, + ReportStatus.NEEDS_MORE_INFO, + ] as ReportStatus[]).includes(status) +} + +export const isClosedStatus = (status: ReportStatus): boolean => { + return !isOpenStatus(status) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/api/types/user.types.ts b/PROJECTS/bug-bounty-platform/frontend/src/api/types/user.types.ts new file mode 100644 index 00000000..2fd36234 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/api/types/user.types.ts @@ -0,0 +1,127 @@ +// =================== +// © AngelaMos | 2025 +// user.types.ts +// =================== + +import { z } from 'zod' +import { PASSWORD_CONSTRAINTS } from '@/config' +import { UserRole, userResponseSchema } from './auth.types' + +export { type UserResponse, UserRole, userResponseSchema } from './auth.types' + +export const userListResponseSchema = z.object({ + items: z.array(userResponseSchema), + total: z.number(), + page: z.number(), + size: z.number(), +}) + +export const userCreateRequestSchema = z.object({ + email: z.string().email(), + password: z + .string() + .min(PASSWORD_CONSTRAINTS.MIN_LENGTH) + .max(PASSWORD_CONSTRAINTS.MAX_LENGTH), + full_name: z.string().max(255).nullable().optional(), +}) + +export const userUpdateRequestSchema = z.object({ + full_name: z.string().max(255).nullable().optional(), +}) + +export const adminUserCreateRequestSchema = z.object({ + email: z.string().email(), + password: z + .string() + .min(PASSWORD_CONSTRAINTS.MIN_LENGTH) + .max(PASSWORD_CONSTRAINTS.MAX_LENGTH), + full_name: z.string().max(255).nullable().optional(), + role: z.nativeEnum(UserRole).optional(), + is_active: z.boolean().optional(), + is_verified: z.boolean().optional(), +}) + +export const adminUserUpdateRequestSchema = z.object({ + email: z.string().email().optional(), + full_name: z.string().max(255).nullable().optional(), + role: z.nativeEnum(UserRole).optional(), + is_active: z.boolean().optional(), + is_verified: z.boolean().optional(), +}) + +export const paginationParamsSchema = z.object({ + page: z.number().min(1), + size: z.number().min(1).max(100), +}) + +export type UserListResponse = z.infer +export type UserCreateRequest = z.infer +export type UserUpdateRequest = z.infer +export type AdminUserCreateRequest = z.infer +export type AdminUserUpdateRequest = z.infer +export type PaginationParams = z.infer + +export const isValidUserListResponse = ( + data: unknown +): data is UserListResponse => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = userListResponseSchema.safeParse(data) + return result.success +} + +export const isValidUserCreateRequest = ( + data: unknown +): data is UserCreateRequest => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = userCreateRequestSchema.safeParse(data) + return result.success +} + +export const isValidAdminUserCreateRequest = ( + data: unknown +): data is AdminUserCreateRequest => { + if (data === null || data === undefined) return false + if (typeof data !== 'object') return false + + const result = adminUserCreateRequestSchema.safeParse(data) + return result.success +} + +export class UserResponseError extends Error { + readonly endpoint?: string + + constructor(message: string, endpoint?: string) { + super(message) + this.name = 'UserResponseError' + this.endpoint = endpoint + Object.setPrototypeOf(this, UserResponseError.prototype) + } +} + +export const USER_ERROR_MESSAGES = { + INVALID_USER_RESPONSE: 'Invalid user data from server', + INVALID_USER_LIST_RESPONSE: 'Invalid user list from server', + USER_NOT_FOUND: 'User not found', + EMAIL_ALREADY_EXISTS: 'Email already exists', + FAILED_TO_CREATE: 'Failed to create user', + FAILED_TO_UPDATE: 'Failed to update user', + FAILED_TO_DELETE: 'Failed to delete user', +} as const + +export const USER_SUCCESS_MESSAGES = { + CREATED: 'User created successfully', + UPDATED: 'User updated successfully', + DELETED: 'User deleted successfully', + PROFILE_UPDATED: 'Profile updated successfully', + REGISTERED: + 'Registration successful! Please check your email to verify your account.', +} as const + +export type UserErrorMessage = + (typeof USER_ERROR_MESSAGES)[keyof typeof USER_ERROR_MESSAGES] +export type UserSuccessMessage = + (typeof USER_SUCCESS_MESSAGES)[keyof typeof USER_SUCCESS_MESSAGES] diff --git a/PROJECTS/bug-bounty-platform/frontend/src/config.ts b/PROJECTS/bug-bounty-platform/frontend/src/config.ts new file mode 100644 index 00000000..936d1513 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/config.ts @@ -0,0 +1,208 @@ +// =================== +// © AngelaMos | 2026 +// config.ts +// =================== +const API_VERSION = 'v1' + +export const API_ENDPOINTS = { + AUTH: { + LOGIN: `/${API_VERSION}/auth/login`, + REFRESH: `/${API_VERSION}/auth/refresh`, + LOGOUT: `/${API_VERSION}/auth/logout`, + LOGOUT_ALL: `/${API_VERSION}/auth/logout-all`, + ME: `/${API_VERSION}/auth/me`, + CHANGE_PASSWORD: `/${API_VERSION}/auth/change-password`, + }, + USERS: { + BASE: `/${API_VERSION}/users`, + BY_ID: (id: string) => `/${API_VERSION}/users/${id}`, + ME: `/${API_VERSION}/users/me`, + REGISTER: `/${API_VERSION}/users`, + }, + PROGRAMS: { + LIST: `/${API_VERSION}/programs`, + MINE: `/${API_VERSION}/programs/mine`, + CREATE: `/${API_VERSION}/programs`, + BY_SLUG: (slug: string) => `/${API_VERSION}/programs/${slug}`, + BY_ID: (id: string) => `/${API_VERSION}/programs/${id}`, + ASSETS: (id: string) => `/${API_VERSION}/programs/${id}/assets`, + ASSET: (programId: string, assetId: string) => + `/${API_VERSION}/programs/${programId}/assets/${assetId}`, + REWARDS: (id: string) => `/${API_VERSION}/programs/${id}/rewards`, + }, + REPORTS: { + LIST: `/${API_VERSION}/reports`, + INBOX: `/${API_VERSION}/reports/inbox`, + STATS: `/${API_VERSION}/reports/stats`, + SUBMIT: `/${API_VERSION}/reports`, + BY_PROGRAM: (programId: string) => + `/${API_VERSION}/reports/program/${programId}`, + BY_ID: (id: string) => `/${API_VERSION}/reports/${id}`, + TRIAGE: (id: string) => `/${API_VERSION}/reports/${id}/triage`, + COMMENTS: (id: string) => `/${API_VERSION}/reports/${id}/comments`, + }, + ADMIN: { + STATS: `/${API_VERSION}/admin/stats`, + USERS: { + LIST: `/${API_VERSION}/admin/users`, + CREATE: `/${API_VERSION}/admin/users`, + BY_ID: (id: string) => `/${API_VERSION}/admin/users/${id}`, + UPDATE: (id: string) => `/${API_VERSION}/admin/users/${id}`, + DELETE: (id: string) => `/${API_VERSION}/admin/users/${id}`, + }, + PROGRAMS: { + LIST: `/${API_VERSION}/admin/programs`, + BY_ID: (id: string) => `/${API_VERSION}/admin/programs/${id}`, + UPDATE: (id: string) => `/${API_VERSION}/admin/programs/${id}`, + DELETE: (id: string) => `/${API_VERSION}/admin/programs/${id}`, + }, + REPORTS: { + LIST: `/${API_VERSION}/admin/reports`, + BY_ID: (id: string) => `/${API_VERSION}/admin/reports/${id}`, + UPDATE: (id: string) => `/${API_VERSION}/admin/reports/${id}`, + }, + }, +} as const + +export const QUERY_KEYS = { + AUTH: { + ALL: ['auth'] as const, + ME: () => [...QUERY_KEYS.AUTH.ALL, 'me'] as const, + }, + USERS: { + ALL: ['users'] as const, + BY_ID: (id: string) => [...QUERY_KEYS.USERS.ALL, 'detail', id] as const, + ME: () => [...QUERY_KEYS.USERS.ALL, 'me'] as const, + }, + PROGRAMS: { + ALL: ['programs'] as const, + LIST: (page: number, size: number) => + [...QUERY_KEYS.PROGRAMS.ALL, 'list', { page, size }] as const, + MINE: (page: number, size: number) => + [...QUERY_KEYS.PROGRAMS.ALL, 'mine', { page, size }] as const, + BY_SLUG: (slug: string) => + [...QUERY_KEYS.PROGRAMS.ALL, 'detail', slug] as const, + }, + REPORTS: { + ALL: ['reports'] as const, + LIST: (page: number, size: number) => + [...QUERY_KEYS.REPORTS.ALL, 'list', { page, size }] as const, + INBOX: (page: number, size: number) => + [...QUERY_KEYS.REPORTS.ALL, 'inbox', { page, size }] as const, + STATS: () => [...QUERY_KEYS.REPORTS.ALL, 'stats'] as const, + BY_ID: (id: string) => [...QUERY_KEYS.REPORTS.ALL, 'detail', id] as const, + BY_PROGRAM: (programId: string, page: number, size: number) => + [...QUERY_KEYS.REPORTS.ALL, 'program', programId, { page, size }] as const, + }, + ADMIN: { + ALL: ['admin'] as const, + STATS: () => [...QUERY_KEYS.ADMIN.ALL, 'stats'] as const, + USERS: { + ALL: () => [...QUERY_KEYS.ADMIN.ALL, 'users'] as const, + LIST: (page: number, size: number, role?: string) => + [...QUERY_KEYS.ADMIN.USERS.ALL(), 'list', { page, size, role }] as const, + BY_ID: (id: string) => + [...QUERY_KEYS.ADMIN.USERS.ALL(), 'detail', id] as const, + }, + PROGRAMS: { + ALL: () => [...QUERY_KEYS.ADMIN.ALL, 'programs'] as const, + LIST: (page: number, size: number, status?: string) => + [ + ...QUERY_KEYS.ADMIN.PROGRAMS.ALL(), + 'list', + { page, size, status }, + ] as const, + }, + REPORTS: { + ALL: () => [...QUERY_KEYS.ADMIN.ALL, 'reports'] as const, + LIST: (page: number, size: number, status?: string, severity?: string) => + [ + ...QUERY_KEYS.ADMIN.REPORTS.ALL(), + 'list', + { page, size, status, severity }, + ] as const, + }, + }, +} as const + +export const ROUTES = { + HOME: '/', + LOGIN: '/login', + REGISTER: '/register', + DASHBOARD: '/dashboard', + SETTINGS: '/settings', + UNAUTHORIZED: '/unauthorized', + PROGRAMS: { + LIST: '/programs', + DETAIL: (slug: string) => `/programs/${slug}`, + SUBMIT: (slug: string) => `/programs/${slug}/submit`, + }, + REPORTS: { + LIST: '/reports', + DETAIL: (id: string) => `/reports/${id}`, + }, + COMPANY: { + DASHBOARD: '/company', + PROGRAMS: '/company/programs', + NEW_PROGRAM: '/company/programs/new', + EDIT_PROGRAM: (slug: string) => `/company/programs/${slug}/edit`, + INBOX: '/company/inbox', + REPORT: (id: string) => `/company/reports/${id}`, + }, + ADMIN: { + DASHBOARD: '/admin', + USERS: '/admin/users', + USER_DETAIL: (id: string) => `/admin/users/${id}`, + PROGRAMS: '/admin/programs', + REPORTS: '/admin/reports', + }, +} as const + +export const STORAGE_KEYS = { + AUTH: 'auth-storage', + UI: 'ui-storage', +} as const + +export const QUERY_CONFIG = { + STALE_TIME: { + USER: 0, + STATIC: Infinity, + FREQUENT: 1000 * 30, + }, + GC_TIME: { + DEFAULT: 1000 * 60 * 30, + LONG: 1000 * 60 * 60, + }, + RETRY: { + DEFAULT: 3, + NONE: 0, + }, +} as const + +export const HTTP_STATUS = { + OK: 200, + CREATED: 201, + NO_CONTENT: 204, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + TOO_MANY_REQUESTS: 429, + INTERNAL_SERVER: 500, +} as const + +export const PASSWORD_CONSTRAINTS = { + MIN_LENGTH: 8, + MAX_LENGTH: 128, +} as const + +export const PAGINATION = { + DEFAULT_PAGE: 1, + DEFAULT_SIZE: 20, + MAX_SIZE: 100, +} as const + +export type ApiEndpoint = typeof API_ENDPOINTS +export type QueryKey = typeof QUERY_KEYS +export type Route = typeof ROUTES diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/api/api.config.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/api/api.config.ts new file mode 100644 index 00000000..8347fa00 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/api/api.config.ts @@ -0,0 +1,160 @@ +// =================== +// © AngelaMos | 2025 +// api.config.ts +// =================== + +import axios, { + type AxiosError, + type AxiosInstance, + type InternalAxiosRequestConfig, +} from 'axios' +import { API_ENDPOINTS, HTTP_STATUS } from '@/config' +import { useAuthStore } from '@/core/lib' +import { ApiError, ApiErrorCode, transformAxiosError } from './errors' + +interface RequestConfig extends InternalAxiosRequestConfig { + _retry?: boolean +} + +interface RefreshSubscriber { + resolve: (token: string) => void + reject: (error: Error) => void +} + +const getBaseURL = (): string => { + return import.meta.env.VITE_API_URL ?? '/api' +} + +export const apiClient: AxiosInstance = axios.create({ + baseURL: getBaseURL(), + timeout: 15000, + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, +}) + +let isRefreshing = false +let refreshSubscribers: RefreshSubscriber[] = [] + +const processRefreshQueue = (error: Error | null, token: string | null): void => { + refreshSubscribers.forEach((subscriber) => { + if (error !== null) { + subscriber.reject(error) + } else if (token !== null) { + subscriber.resolve(token) + } + }) + refreshSubscribers = [] +} + +const addRefreshSubscriber = ( + resolve: (token: string) => void, + reject: (error: Error) => void +): void => { + refreshSubscribers.push({ resolve, reject }) +} + +const handleTokenRefresh = async (): Promise => { + const response = await apiClient.post<{ access_token: string }>( + API_ENDPOINTS.AUTH.REFRESH + ) + + if ( + response.data === null || + response.data === undefined || + typeof response.data !== 'object' + ) { + throw new ApiError( + 'Invalid refresh response', + ApiErrorCode.AUTHENTICATION_ERROR, + HTTP_STATUS.UNAUTHORIZED + ) + } + + const accessToken = response.data.access_token + if (typeof accessToken !== 'string' || accessToken.length === 0) { + throw new ApiError( + 'Invalid access token', + ApiErrorCode.AUTHENTICATION_ERROR, + HTTP_STATUS.UNAUTHORIZED + ) + } + + return accessToken +} + +const handleAuthFailure = (): void => { + useAuthStore.getState().logout() + window.location.href = '/login' +} + +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { + const token = useAuthStore.getState().accessToken + if (token !== null && token.length > 0) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error: unknown): Promise => { + return Promise.reject(error) + } +) + +apiClient.interceptors.response.use( + (response) => response, + async (error: AxiosError): Promise => { + const originalRequest = error.config as RequestConfig | undefined + + if (originalRequest === undefined) { + return Promise.reject(transformAxiosError(error)) + } + + const isUnauthorized = error.response?.status === HTTP_STATUS.UNAUTHORIZED + const isNotRetried = originalRequest._retry !== true + const isNotRefreshEndpoint = + originalRequest.url?.includes(API_ENDPOINTS.AUTH.REFRESH) !== true + + if (isUnauthorized && isNotRetried && isNotRefreshEndpoint) { + if (isRefreshing) { + return new Promise((resolve, reject) => { + addRefreshSubscriber( + (newToken: string): void => { + originalRequest.headers.Authorization = `Bearer ${newToken}` + resolve(apiClient(originalRequest)) + }, + (refreshError: Error): void => { + reject(refreshError) + } + ) + }) + } + + originalRequest._retry = true + isRefreshing = true + + try { + const newToken = await handleTokenRefresh() + useAuthStore.getState().setAccessToken(newToken) + processRefreshQueue(null, newToken) + originalRequest.headers.Authorization = `Bearer ${newToken}` + return await apiClient(originalRequest) + } catch (refreshError: unknown) { + const apiError = + refreshError instanceof ApiError + ? refreshError + : new ApiError( + 'Session expired', + ApiErrorCode.AUTHENTICATION_ERROR, + HTTP_STATUS.UNAUTHORIZED + ) + processRefreshQueue(apiError, null) + handleAuthFailure() + return Promise.reject(apiError) + } finally { + isRefreshing = false + } + } + + return Promise.reject(transformAxiosError(error)) + } +) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/api/errors.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/api/errors.ts new file mode 100644 index 00000000..fde5ec65 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/api/errors.ts @@ -0,0 +1,114 @@ +/** + * ©AngelaMos | 2025 + * errors.ts + */ + +import type { AxiosError } from 'axios' + +export const ApiErrorCode = { + NETWORK_ERROR: 'NETWORK_ERROR', + VALIDATION_ERROR: 'VALIDATION_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + CONFLICT: 'CONFLICT', + RATE_LIMITED: 'RATE_LIMITED', + SERVER_ERROR: 'SERVER_ERROR', + UNKNOWN_ERROR: 'UNKNOWN_ERROR', +} as const + +export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode] + +export class ApiError extends Error { + readonly code: ApiErrorCode + readonly statusCode: number + readonly details?: Record + + constructor( + message: string, + code: ApiErrorCode, + statusCode: number, + details?: Record + ) { + super(message) + this.name = 'ApiError' + this.code = code + this.statusCode = statusCode + this.details = details + } + + getUserMessage(): string { + const messages: Record = { + [ApiErrorCode.NETWORK_ERROR]: + 'Unable to connect. Please check your internet connection.', + [ApiErrorCode.VALIDATION_ERROR]: 'Please check your input and try again.', + [ApiErrorCode.AUTHENTICATION_ERROR]: + 'Your session has expired. Please log in again.', + [ApiErrorCode.AUTHORIZATION_ERROR]: + 'You do not have permission to perform this action.', + [ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.', + [ApiErrorCode.CONFLICT]: + 'This operation conflicts with an existing resource.', + [ApiErrorCode.RATE_LIMITED]: + 'Too many requests. Please wait a moment and try again.', + [ApiErrorCode.SERVER_ERROR]: + 'Something went wrong on our end. Please try again later.', + [ApiErrorCode.UNKNOWN_ERROR]: + 'An unexpected error occurred. Please try again.', + } + return messages[this.code] + } +} + +interface ApiErrorResponse { + detail?: string | { msg: string; type: string }[] + message?: string +} + +export function transformAxiosError(error: AxiosError): ApiError { + if (!error.response) { + return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0) + } + + const { status } = error.response + const data = error.response.data as ApiErrorResponse | undefined + let message = 'An error occurred' + let details: Record | undefined + + if (data?.detail) { + if (typeof data.detail === 'string') { + message = data.detail + } else if (Array.isArray(data.detail)) { + details = { validation: [] } + data.detail.forEach((err) => { + details?.validation.push(err.msg) + }) + message = 'Validation error' + } + } else if (data?.message) { + message = data.message + } + + const codeMap: Record = { + 400: ApiErrorCode.VALIDATION_ERROR, + 401: ApiErrorCode.AUTHENTICATION_ERROR, + 403: ApiErrorCode.AUTHORIZATION_ERROR, + 404: ApiErrorCode.NOT_FOUND, + 409: ApiErrorCode.CONFLICT, + 429: ApiErrorCode.RATE_LIMITED, + 500: ApiErrorCode.SERVER_ERROR, + 502: ApiErrorCode.SERVER_ERROR, + 503: ApiErrorCode.SERVER_ERROR, + 504: ApiErrorCode.SERVER_ERROR, + } + + const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR + + return new ApiError(message, code, status, details) +} + +declare module '@tanstack/react-query' { + interface Register { + defaultError: ApiError + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/api/index.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/api/index.ts new file mode 100644 index 00000000..1818cd7e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/api/index.ts @@ -0,0 +1,8 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './api.config' +export * from './errors' +export * from './query.config' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/api/query.config.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/api/query.config.ts new file mode 100644 index 00000000..8244b22d --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/api/query.config.ts @@ -0,0 +1,105 @@ +// =================== +// © AngelaMos | 2025 +// query.config.ts +// =================== + +import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { QUERY_CONFIG } from '@/config' +import { ApiError, ApiErrorCode } from './errors' + +const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [ + ApiErrorCode.AUTHENTICATION_ERROR, + ApiErrorCode.AUTHORIZATION_ERROR, + ApiErrorCode.NOT_FOUND, + ApiErrorCode.VALIDATION_ERROR, +] as const + +const shouldRetryQuery = (failureCount: number, error: Error): boolean => { + if (error instanceof ApiError) { + if (NO_RETRY_ERROR_CODES.includes(error.code)) { + return false + } + } + return failureCount < QUERY_CONFIG.RETRY.DEFAULT +} + +const calculateRetryDelay = (attemptIndex: number): number => { + const baseDelay = 1000 + const maxDelay = 30000 + return Math.min(baseDelay * 2 ** attemptIndex, maxDelay) +} + +const handleQueryCacheError = ( + error: Error, + query: { state: { data: unknown } } +): void => { + if (query.state.data !== undefined) { + const message = + error instanceof ApiError + ? error.getUserMessage() + : 'Background update failed' + toast.error(message) + } +} + +const handleMutationCacheError = ( + error: Error, + _variables: unknown, + _context: unknown, + mutation: { options: { onError?: unknown } } +): void => { + if (mutation.options.onError === undefined) { + const message = + error instanceof ApiError ? error.getUserMessage() : 'Operation failed' + toast.error(message) + } +} + +export const QUERY_STRATEGIES = { + standard: { + staleTime: QUERY_CONFIG.STALE_TIME.USER, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + }, + frequent: { + staleTime: QUERY_CONFIG.STALE_TIME.FREQUENT, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + refetchInterval: QUERY_CONFIG.STALE_TIME.FREQUENT, + }, + static: { + staleTime: QUERY_CONFIG.STALE_TIME.STATIC, + gcTime: QUERY_CONFIG.GC_TIME.LONG, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + auth: { + staleTime: QUERY_CONFIG.STALE_TIME.USER, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: QUERY_CONFIG.RETRY.NONE, + }, +} as const + +export type QueryStrategy = keyof typeof QUERY_STRATEGIES + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: QUERY_CONFIG.STALE_TIME.USER, + gcTime: QUERY_CONFIG.GC_TIME.DEFAULT, + retry: shouldRetryQuery, + retryDelay: calculateRetryDelay, + refetchOnWindowFocus: true, + refetchOnMount: true, + refetchOnReconnect: true, + }, + mutations: { + retry: QUERY_CONFIG.RETRY.NONE, + }, + }, + queryCache: new QueryCache({ + onError: handleQueryCacheError, + }), + mutationCache: new MutationCache({ + onError: handleMutationCacheError, + }), +}) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/app/protected-route.tsx b/PROJECTS/bug-bounty-platform/frontend/src/core/app/protected-route.tsx new file mode 100644 index 00000000..486eec78 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/app/protected-route.tsx @@ -0,0 +1,47 @@ +// =================== +// © AngelaMos | 2025 +// protected-route.tsx +// =================== + +import { Navigate, Outlet, useLocation } from 'react-router-dom' +import type { UserRole } from '@/api/types' +import { ROUTES } from '@/config' +import { useAuthStore } from '@/core/lib' + +interface ProtectedRouteProps { + allowedRoles?: UserRole[] + redirectTo?: string +} + +export function ProtectedRoute({ + allowedRoles, + redirectTo = ROUTES.LOGIN, +}: ProtectedRouteProps): React.ReactElement { + const location = useLocation() + const { isAuthenticated, isLoading, user } = useAuthStore() + + if (isLoading) { + return
Loading...
+ } + + if (!isAuthenticated) { + return ( + + ) + } + + if ( + allowedRoles !== undefined && + allowedRoles.length > 0 && + user !== null && + !allowedRoles.includes(user.role) + ) { + return + } + + return +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/app/routers.tsx b/PROJECTS/bug-bounty-platform/frontend/src/core/app/routers.tsx new file mode 100644 index 00000000..1701d48a --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/app/routers.tsx @@ -0,0 +1,119 @@ +// =================== +// AngelaMos | 2026 +// routers.tsx +// =================== + +import { createBrowserRouter, type RouteObject } from 'react-router-dom' +import { UserRole } from '@/api/types' +import { ROUTES } from '@/config' +import { ProtectedRoute } from './protected-route' +import { Shell } from './shell' + +const routes: RouteObject[] = [ + { + path: ROUTES.HOME, + lazy: () => import('@/routes/landing'), + }, + { + path: ROUTES.LOGIN, + lazy: () => import('@/routes/login'), + }, + { + path: ROUTES.REGISTER, + lazy: () => import('@/routes/register'), + }, + { + element: , + children: [ + { + element: , + children: [ + { + path: ROUTES.DASHBOARD, + lazy: () => import('@/routes/dashboard'), + }, + { + path: ROUTES.SETTINGS, + lazy: () => import('@/routes/settings'), + }, + { + path: ROUTES.PROGRAMS.LIST, + lazy: () => import('@/routes/programs'), + }, + { + path: '/programs/:slug', + lazy: () => import('@/routes/programs/[slug]'), + }, + { + path: '/programs/:slug/submit', + lazy: () => import('@/routes/programs/[slug]/submit'), + }, + { + path: ROUTES.REPORTS.LIST, + lazy: () => import('@/routes/reports'), + }, + { + path: '/reports/:id', + lazy: () => import('@/routes/reports/[id]'), + }, + { + path: ROUTES.COMPANY.PROGRAMS, + lazy: () => import('@/routes/company/programs'), + }, + { + path: ROUTES.COMPANY.NEW_PROGRAM, + lazy: () => import('@/routes/company/programs/new'), + }, + { + path: '/company/programs/:slug/edit', + lazy: () => import('@/routes/company/programs/[id]/edit'), + }, + { + path: ROUTES.COMPANY.INBOX, + lazy: () => import('@/routes/company/inbox'), + }, + { + path: '/company/reports/:id', + lazy: () => import('@/routes/company/reports/[id]'), + }, + ], + }, + ], + }, + { + element: , + children: [ + { + element: , + children: [ + { + path: ROUTES.ADMIN.DASHBOARD, + lazy: () => import('@/routes/admin/stats'), + }, + { + path: ROUTES.ADMIN.USERS, + lazy: () => import('@/routes/admin'), + }, + { + path: ROUTES.ADMIN.PROGRAMS, + lazy: () => import('@/routes/admin/programs'), + }, + { + path: ROUTES.ADMIN.REPORTS, + lazy: () => import('@/routes/admin/reports'), + }, + ], + }, + ], + }, + { + path: ROUTES.UNAUTHORIZED, + lazy: () => import('@/routes/landing'), + }, + { + path: '*', + lazy: () => import('@/routes/landing'), + }, +] + +export const router = createBrowserRouter(routes) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.module.scss new file mode 100644 index 00000000..4946285e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.module.scss @@ -0,0 +1,375 @@ +// =================== +// © AngelaMos | 2025 +// shell.module.scss +// =================== + +@use '@/styles' as *; + +$sidebar-width: 240px; +$sidebar-collapsed-width: 64px; +$header-height: 56px; + +.shell { + display: flex; + min-height: 100vh; + min-height: 100dvh; +} + +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: $sidebar-width; + background-color: $bg-shell-base; + background-image: radial-gradient( + circle, + $bg-shell-dot 1px, + transparent 1px + ); + background-size: 17px 17px; + border-right: 1px solid $border-default; + display: flex; + flex-direction: column; + z-index: $z-fixed; + @include transition-fast; + + &.collapsed { + width: $sidebar-collapsed-width; + } + + @include breakpoint-down('sm') { + transform: translateX(-100%); + + &.open { + transform: translateX(0); + } + + &.collapsed { + width: $sidebar-width; + } + } +} + +.sidebarHeader { + height: $header-height; + padding: 0 $space-3; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid $border-default; + + .sidebar.collapsed & { + justify-content: center; + padding: 0; + } +} + +.logo { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-default; + @include transition-fast; + + .sidebar.collapsed & { + display: none; + } +} + +.nav { + flex: 1; + padding: $space-3; + display: flex; + flex-direction: column; + gap: $space-1; +} + +.navItem { + display: flex; + align-items: center; + gap: $space-3; + padding: $space-2 $space-3; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-light; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &.active { + background: $bg-selection; + color: $text-default; + } + + .sidebar.collapsed & { + justify-content: center; + } +} + +.navIcon { + width: 17px; + height: 17px; + flex-shrink: 0; + color:$accent-orange; +} + +.navLabel { + @include transition-fast; + + .sidebar.collapsed & { + display: none; + } +} + +.navDivider { + height: 1px; + background: $border-muted; + margin: $space-2 0; + + .sidebar.collapsed & { + margin: $space-2 $space-1; + } +} + +.navSection { + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-muted; + text-transform: uppercase; + letter-spacing: $tracking-wide; + padding: $space-2 $space-3; + + .sidebar.collapsed & { + display: none; + } +} + +.adminItem { + margin-top: auto; + border-top: 1px solid $border-default; + padding-top: $space-3; +} + +.collapseBtn { + width: 45px; + height: 45px; + border-radius: $radius-md; + color: $accent-orange;; + @include flex-center; + @include transition-fast; + + svg { + width: 23.5px; + height: 23.5px; + } + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + @include breakpoint-down('sm') { + display: none; + } +} + +.sidebarFooter { + padding: $space-3; + border-top: 1px solid $border-default; +} + +.logoutBtn { + width: 100%; + display: flex; + align-items: center; + gap: $space-3; + padding: $space-3; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + } + + .sidebar.collapsed & { + justify-content: center; + + .logoutText { + display: none; + } + } +} + +.logoutIcon { + width: 18px; + height: 18px; + flex-shrink: 0; + color:$accent-orange; +} + +.logoutText { + font-weight: $font-weight-medium; + @include transition-fast; +} + +.overlay { + position: fixed; + inset: 0; + background: rgb(0, 0, 0, 50%); + z-index: calc($z-fixed - 1); + display: none; + border: none; + padding: 0; + cursor: pointer; + + @include breakpoint-down('sm') { + display: block; + } +} + +.main { + flex: 1; + display: flex; + flex-direction: column; + margin-left: $sidebar-width; + min-width: 0; + @include transition-fast; + + &.collapsed { + margin-left: $sidebar-collapsed-width; + } + + @include breakpoint-down('sm') { + margin-left: 0; + + &.collapsed { + margin-left: 0; + } + } +} + +.header { + position: sticky; + top: 0; + height: $header-height; + background-color: $bg-shell-base; + background-image: radial-gradient( + circle, + $bg-shell-dot 1px, + transparent 1px + ); + background-size: 17px 17px; + border-bottom: 1px solid $border-default; + z-index: $z-sticky; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 $space-4; +} + +.headerLeft { + display: flex; + align-items: center; + gap: $space-3; +} + +.menuBtn { + display: none; + width: 36px; + height: 36px; + border-radius: $radius-md; + color: $text-light; + align-items: center; + justify-content: center; + @include transition-fast; + + svg { + width: 20px; + height: 20px; + } + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + @media (width <= 479px) { + display: flex; + } +} + +.pageTitle { + font-size: $font-size-base; + font-weight: $font-weight-medium; + color: $text-default; + margin-left: 7px; +} + +.headerRight { + display: flex; + align-items: center; + gap: $space-3; +} + +.avatar { + width: 32px; + height: 32px; + border-radius: $radius-full; + background: $bg-surface-300; + color:$accent-orange; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + @include flex-center; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(1.2); + } +} + +.content { + flex: 1; + overflow-y: auto; + background-color: $bg-content-base; + background-image: radial-gradient( + circle, + $bg-content-dot 1px, + transparent 1px + ); + background-size: 20px 20px; +} + +.loading { + @include flex-center; + height: 100%; + color: $text-muted; +} + +.error { + @include flex-column-center; + height: 100%; + gap: $space-4; + padding: $space-6; + color: $error-default; + + h2 { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + } + + pre { + font-family: $font-mono; + font-size: $font-size-sm; + padding: $space-4; + background: $bg-surface-200; + border-radius: $radius-lg; + overflow-x: auto; + max-width: 100%; + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.tsx b/PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.tsx new file mode 100644 index 00000000..23c3a23e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/app/shell.tsx @@ -0,0 +1,227 @@ +/** + * AngelaMos | 2026 + * shell.tsx + */ + +import { Suspense } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { + LuBuilding2, + LuChartNetwork, + LuChevronLeft, + LuChevronRight, + LuFileText, + LuInbox, + LuLayoutDashboard, + LuLogOut, + LuMenu, + LuSettings, + LuTarget, + LuUsers, +} from 'react-icons/lu' +import { Link, NavLink, Outlet, useLocation } from 'react-router-dom' +import { useLogout } from '@/api/hooks' +import { ROUTES } from '@/config' +import { useIsAdmin, useUIStore, useUser } from '@/core/lib' +import styles from './shell.module.scss' + +const NAV_ITEMS = [ + { path: ROUTES.DASHBOARD, label: 'Dashboard', icon: LuLayoutDashboard }, + { path: ROUTES.PROGRAMS.LIST, label: 'Programs', icon: LuTarget }, + { path: ROUTES.REPORTS.LIST, label: 'My Reports', icon: LuFileText }, + { path: ROUTES.SETTINGS, label: 'Settings', icon: LuSettings }, +] + +const COMPANY_NAV_ITEMS = [ + { path: ROUTES.COMPANY.PROGRAMS, label: 'My Programs', icon: LuBuilding2 }, + { path: ROUTES.COMPANY.INBOX, label: 'Inbox', icon: LuInbox }, +] + +const ADMIN_NAV_ITEMS = [ + { path: ROUTES.ADMIN.DASHBOARD, label: 'Statistics', icon: LuChartNetwork }, + { path: ROUTES.ADMIN.USERS, label: 'Users', icon: LuUsers }, + { path: ROUTES.ADMIN.PROGRAMS, label: 'Programs', icon: LuTarget }, + { path: ROUTES.ADMIN.REPORTS, label: 'Reports', icon: LuFileText }, +] + +function ShellErrorFallback({ error }: { error: Error }): React.ReactElement { + return ( +
+

Something went wrong

+
{error.message}
+
+ ) +} + +function ShellLoading(): React.ReactElement { + return
Loading...
+} + +function getPageTitle(pathname: string, isAdmin: boolean): string { + if (isAdmin) { + const adminItem = ADMIN_NAV_ITEMS.find((i) => i.path === pathname) + if (adminItem) { + return `Admin: ${adminItem.label}` + } + } + if (pathname.startsWith('/programs/') && pathname.includes('/submit')) { + return 'Submit Report' + } + if (pathname.startsWith('/programs/')) { + return 'Program' + } + if (pathname.startsWith('/reports/')) { + return 'Report' + } + if (pathname === ROUTES.COMPANY.NEW_PROGRAM) { + return 'New Program' + } + if (pathname.startsWith('/company/programs/') && pathname.endsWith('/edit')) { + return 'Edit Program' + } + if (pathname.startsWith('/company/reports/')) { + return 'Triage Report' + } + const companyItem = COMPANY_NAV_ITEMS.find((i) => i.path === pathname) + if (companyItem) { + return companyItem.label + } + const item = NAV_ITEMS.find((i) => i.path === pathname) + return item?.label ?? 'Dashboard' +} + +export function Shell(): React.ReactElement { + const location = useLocation() + const { sidebarOpen, sidebarCollapsed, toggleSidebar, toggleSidebarCollapsed } = + useUIStore() + const { mutate: logout } = useLogout() + const isAdmin = useIsAdmin() + const user = useUser() + + const pageTitle = getPageTitle(location.pathname, isAdmin) + const avatarLetter = + user?.full_name?.[0]?.toUpperCase() ?? user?.email?.[0]?.toUpperCase() ?? 'U' + + return ( +
+ + + {sidebarOpen && ( + +

{pageTitle}

+
+ +
+ + {avatarLetter} + +
+ + +
+ + }> + + + +
+ + + ) +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/app/toast.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/core/app/toast.module.scss new file mode 100644 index 00000000..d50ab7fb --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/app/toast.module.scss @@ -0,0 +1,67 @@ +// =================== +// © AngelaMos | 2025 +// toast.module.scss +// =================== + +@use '@/styles' as *; + +:global { + [data-sonner-toaster] { + --normal-bg: #{$bg-surface-100}; + --normal-border: #{$border-default}; + --normal-text: #{$text-default}; + + --success-bg: #{$bg-surface-100}; + --success-border: #{$border-default}; + --success-text: #{$text-default}; + + --error-bg: #{$bg-surface-100}; + --error-border: #{$error-default}; + --error-text: #{$text-default}; + + --warning-bg: #{$bg-surface-100}; + --warning-border: #{$border-default}; + --warning-text: #{$text-default}; + + --info-bg: #{$bg-surface-100}; + --info-border: #{$border-default}; + --info-text: #{$text-default}; + + font-family: $font-sans; + } + + [data-sonner-toast] { + border-radius: $radius-md; + padding: $space-3 $space-4; + font-size: $font-size-sm; + border: 1px solid $border-default; + background: $bg-surface-100; + color: $text-default; + + [data-title] { + font-weight: $font-weight-medium; + } + + [data-description] { + color: $text-light; + font-size: $font-size-xs; + } + + [data-close-button] { + background: none; + border: none; + padding: 0; + cursor: pointer; + color: $text-muted; + @include transition-fast; + + @include hover { + color: $text-default; + } + } + } + + [data-sonner-toast][data-type='error'] { + border-color: $error-default; + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.form.store.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.form.store.ts new file mode 100644 index 00000000..22c422c5 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.form.store.ts @@ -0,0 +1,43 @@ +/** + * ©AngelaMos | 2025 + * auth.form.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +interface AuthFormState { + loginEmail: string + registerEmail: string + setLoginEmail: (email: string) => void + setRegisterEmail: (email: string) => void + clearLoginForm: () => void + clearRegisterForm: () => void +} + +export const useAuthFormStore = create()( + devtools( + persist( + (set) => ({ + loginEmail: '', + registerEmail: '', + + setLoginEmail: (email) => + set({ loginEmail: email }, false, 'authForm/setLoginEmail'), + + setRegisterEmail: (email) => + set({ registerEmail: email }, false, 'authForm/setRegisterEmail'), + + clearLoginForm: () => + set({ loginEmail: '' }, false, 'authForm/clearLoginForm'), + + clearRegisterForm: () => + set({ registerEmail: '' }, false, 'authForm/clearRegisterForm'), + }), + { + name: 'auth-form-storage', + } + ), + { name: 'AuthFormStore' } + ) +) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.store.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.store.ts new file mode 100644 index 00000000..a02cd0c9 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/auth.store.ts @@ -0,0 +1,105 @@ +// =================== +// © AngelaMos | 2025 +// auth.store.ts +// =================== + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' +import { type UserResponse, UserRole } from '@/api/types' +import { STORAGE_KEYS } from '@/config' + +interface AuthState { + user: UserResponse | null + accessToken: string | null + isAuthenticated: boolean + isLoading: boolean +} + +interface AuthActions { + login: (user: UserResponse, accessToken: string) => void + logout: () => void + setLoading: (loading: boolean) => void + setAccessToken: (token: string | null) => void + updateUser: (updates: Partial) => void +} + +type AuthStore = AuthState & AuthActions + +export const useAuthStore = create()( + devtools( + persist( + (set) => ({ + user: null, + accessToken: null, + isAuthenticated: false, + isLoading: false, + + login: (user, accessToken) => + set( + { + user, + accessToken, + isAuthenticated: true, + isLoading: false, + }, + false, + 'auth/login' + ), + + logout: () => + set( + { + user: null, + accessToken: null, + isAuthenticated: false, + isLoading: false, + }, + false, + 'auth/logout' + ), + + setLoading: (loading) => + set({ isLoading: loading }, false, 'auth/setLoading'), + + setAccessToken: (token) => + set({ accessToken: token }, false, 'auth/setAccessToken'), + + updateUser: (updates) => + set( + (state) => ({ + user: state.user !== null ? { ...state.user, ...updates } : null, + }), + false, + 'auth/updateUser' + ), + }), + { + name: STORAGE_KEYS.AUTH, + partialize: (state) => ({ + user: state.user, + isAuthenticated: state.isAuthenticated, + }), + } + ), + { name: 'AuthStore' } + ) +) + +export const useUser = (): UserResponse | null => useAuthStore((s) => s.user) +export const useIsAuthenticated = (): boolean => + useAuthStore((s) => s.isAuthenticated) +export const useIsAuthLoading = (): boolean => useAuthStore((s) => s.isLoading) +export const useAccessToken = (): string | null => + useAuthStore((s) => s.accessToken) + +export const useHasRole = (role: UserRole): boolean => { + const user = useAuthStore((s) => s.user) + return user !== null && user.role === role +} + +export const useIsAdmin = (): boolean => { + const user = useAuthStore((s) => s.user) + return user !== null && user.role === UserRole.ADMIN +} + +export { UserRole } diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/lib/index.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/index.ts new file mode 100644 index 00000000..ced5cfc9 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/index.ts @@ -0,0 +1,10 @@ +// =================== +// © AngelaMos | 2025 +// index.ts +// =================== + +export * from './auth.form.store' +export * from './auth.store' +export * from './program.form.store' +export * from './settings.form.store' +export * from './shell.ui.store' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/lib/program.form.store.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/program.form.store.ts new file mode 100644 index 00000000..f07c1248 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/program.form.store.ts @@ -0,0 +1,68 @@ +/** + * ©AngelaMos | 2026 + * program.form.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' +import { ProgramVisibility } from '@/api/types' + +interface ProgramFormState { + name: string + slug: string + description: string + rules: string + responseSlaHours: number + visibility: ProgramVisibility + setName: (name: string) => void + setSlug: (slug: string) => void + setDescription: (description: string) => void + setRules: (rules: string) => void + setResponseSlaHours: (hours: number) => void + setVisibility: (visibility: ProgramVisibility) => void + clearForm: () => void +} + +const initialState = { + name: '', + slug: '', + description: '', + rules: '', + responseSlaHours: 72, + visibility: ProgramVisibility.PUBLIC, +} + +export const useProgramFormStore = create()( + devtools( + persist( + (set) => ({ + ...initialState, + + setName: (name) => set({ name }, false, 'programForm/setName'), + + setSlug: (slug) => set({ slug }, false, 'programForm/setSlug'), + + setDescription: (description) => + set({ description }, false, 'programForm/setDescription'), + + setRules: (rules) => set({ rules }, false, 'programForm/setRules'), + + setResponseSlaHours: (hours) => + set( + { responseSlaHours: hours }, + false, + 'programForm/setResponseSlaHours' + ), + + setVisibility: (visibility) => + set({ visibility }, false, 'programForm/setVisibility'), + + clearForm: () => set(initialState, false, 'programForm/clearForm'), + }), + { + name: 'program-form-storage', + } + ), + { name: 'ProgramFormStore' } + ) +) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/lib/settings.form.store.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/settings.form.store.ts new file mode 100644 index 00000000..d40ebcd8 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/settings.form.store.ts @@ -0,0 +1,36 @@ +/** + * ©AngelaMos | 2026 + * settings.form.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +interface SettingsFormState { + fullName: string + setFullName: (fullName: string) => void + clearForm: () => void +} + +const initialState = { + fullName: '', +} + +export const useSettingsFormStore = create()( + devtools( + persist( + (set) => ({ + ...initialState, + + setFullName: (fullName) => + set({ fullName }, false, 'settingsForm/setFullName'), + + clearForm: () => set(initialState, false, 'settingsForm/clearForm'), + }), + { + name: 'settings-form-storage', + } + ), + { name: 'SettingsFormStore' } + ) +) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/core/lib/shell.ui.store.ts b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/shell.ui.store.ts new file mode 100644 index 00000000..d601a53b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/core/lib/shell.ui.store.ts @@ -0,0 +1,63 @@ +/** + * ©AngelaMos | 2025 + * ui.store.ts + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +type Theme = 'light' | 'dark' | 'system' + +interface UIState { + theme: Theme + sidebarOpen: boolean + sidebarCollapsed: boolean + setTheme: (theme: Theme) => void + toggleSidebar: () => void + setSidebarOpen: (open: boolean) => void + toggleSidebarCollapsed: () => void +} + +export const useUIStore = create()( + devtools( + persist( + (set) => ({ + theme: 'dark', + sidebarOpen: false, + sidebarCollapsed: false, + + setTheme: (theme) => set({ theme }, false, 'ui/setTheme'), + + toggleSidebar: () => + set( + (state) => ({ sidebarOpen: !state.sidebarOpen }), + false, + 'ui/toggleSidebar' + ), + + setSidebarOpen: (open) => + set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'), + + toggleSidebarCollapsed: () => + set( + (state) => ({ sidebarCollapsed: !state.sidebarCollapsed }), + false, + 'ui/toggleSidebarCollapsed' + ), + }), + { + name: 'ui-storage', + partialize: (state) => ({ + theme: state.theme, + sidebarCollapsed: state.sidebarCollapsed, + }), + } + ), + { name: 'UIStore' } + ) +) + +export const useTheme = (): Theme => useUIStore((s) => s.theme) +export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen) +export const useSidebarCollapsed = (): boolean => + useUIStore((s) => s.sidebarCollapsed) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/main.tsx b/PROJECTS/bug-bounty-platform/frontend/src/main.tsx new file mode 100644 index 00000000..ac319510 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/main.tsx @@ -0,0 +1,15 @@ +// =========================== +// ©AngelaMos | 2026 +// main.tsx +// =========================== + +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './styles.scss' + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/admin.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/admin.module.scss new file mode 100644 index 00000000..bb31d56e --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/admin.module.scss @@ -0,0 +1,463 @@ +// =================== +// © AngelaMos | 2025 +// admin.module.scss +// =================== + +@use '@/styles' as *; + +.page { + padding: $space-6; + min-height: calc(100vh - 56px); +} + +.header { + @include flex-between; + margin-bottom: $space-6; +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.createBtn { + display: flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 $space-5; + background-color: $accent-orange; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $white; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(1.1); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.table { + width: 100%; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + overflow: hidden; +} + +.tableHeader { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 100px; + gap: $space-4; + padding: $space-3 $space-4; + background: $bg-surface-200; + border-bottom: 1px solid $border-default; + + @include breakpoint-down('md') { + display: none; + } +} + +.tableHeaderCell { + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wide; +} + +.tableBody { + @include flex-column; +} + +.tableRow { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 100px; + gap: $space-4; + padding: $space-3 $space-4; + border-bottom: 1px solid $border-default; + @include transition-fast; + + &:last-child { + border-bottom: none; + } + + @include hover { + background: $bg-surface-75; + } + + @include breakpoint-down('md') { + grid-template-columns: 1fr; + gap: $space-2; + } +} + +.tableCell { + display: flex; + align-items: center; + font-size: $font-size-sm; + color: $text-default; + min-width: 0; + + @include breakpoint-down('md') { + &::before { + content: attr(data-label); + font-size: $font-size-xs; + color: $text-lighter; + margin-right: $space-2; + min-width: 80px; + } + } +} + +.email { + @include truncate; +} + +.badge { + display: inline-flex; + align-items: center; + padding: $space-1 $space-2; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &.admin { + background: $bg-selection; + color: $text-default; + } + + &.user { + background: $bg-surface-200; + color: $text-light; + } + + &.active { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &.inactive { + background: hsl(0 72% 51% / 20%); + color: $error-light; + } +} + +.actions { + display: flex; + gap: $space-2; + justify-content: flex-end; + + @include breakpoint-down('md') { + justify-content: flex-start; + } +} + +.actionBtn { + width: 32px; + height: 32px; + @include flex-center; + border: 1px solid $border-default; + border-radius: $radius-md; + background: transparent; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &.delete { + @include hover { + border-color: $error-default; + color: $error-default; + } + } + + svg { + width: 16px; + height: 16px; + } +} + +.pagination { + @include flex-between; + padding: $space-4; + border-top: 1px solid $border-default; +} + +.paginationInfo { + font-size: $font-size-sm; + color: $text-lighter; +} + +.paginationBtns { + display: flex; + gap: $space-2; +} + +.paginationBtn { + padding: $space-2 $space-3; + border: 1px solid $border-default; + border-radius: $radius-md; + background: transparent; + font-size: $font-size-sm; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.empty { + @include flex-column-center; + padding: $space-12; + color: $text-muted; + font-size: $font-size-sm; +} + +.loading { + @include flex-center; + padding: $space-12; + color: $text-muted; +} + +.modal { + position: fixed; + inset: 0; + z-index: $z-modal; + @include flex-center; +} + +.modalOverlay { + @include absolute-fill; + background: rgb(0, 0, 0, 70%); +} + +.modalContent { + position: relative; + width: 100%; + max-width: 400px; + margin: $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + padding: $space-6; +} + +.modalHeader { + @include flex-between; + margin-bottom: $space-5; +} + +.modalTitle { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.modalClose { + width: 32px; + height: 32px; + @include flex-center; + border: none; + border-radius: $radius-md; + background: transparent; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + svg { + width: 20px; + height: 20px; + } +} + +.form { + @include flex-column; + gap: $space-4; +} + +.field { + @include flex-column; + gap: $space-2; +} + +.label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; +} + +.input { + width: 100%; + height: 44px; + padding: 0 $space-3; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + @include transition-fast; + + &::placeholder { + color: $text-muted; + } + + &:focus { + outline: none; + border-color: $border-strong; + } +} + +.select { + width: 100%; + height: 44px; + padding: 0 $space-3; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + cursor: pointer; + @include transition-fast; + + &:focus { + outline: none; + border-color: $border-strong; + } + + option { + background: $bg-surface-100; + color: $text-default; + } +} + +.checkbox { + display: flex; + align-items: center; + gap: $space-2; + cursor: pointer; + + input { + width: 18px; + height: 18px; + accent-color: $white; + } + + span { + font-size: $font-size-sm; + color: $text-light; + } +} + +.formActions { + display: flex; + gap: $space-3; + margin-top: $space-2; +} + +.submitBtn { + flex: 1; + height: 44px; + @include flex-center; + background: $white; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $black; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(0.9); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.cancelBtn { + flex: 1; + height: 44px; + @include flex-center; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } +} + +.deleteConfirm { + @include flex-column; + gap: $space-4; +} + +.deleteText { + font-size: $font-size-sm; + color: $text-light; + line-height: $line-height-relaxed; +} + +.deleteEmail { + font-weight: $font-weight-medium; + color: $text-default; +} + +.deleteBtn { + flex: 1; + height: 44px; + @include flex-center; + background: $error-default; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $white; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(0.9); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/index.tsx b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/index.tsx new file mode 100644 index 00000000..401a27ec --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/index.tsx @@ -0,0 +1,414 @@ +/** + * ©AngelaMos | 2025 + * index.tsx + */ + +import { useState } from 'react' +import { LuPencil, LuTrash2, LuX } from 'react-icons/lu' +import { + useAdminCreateUser, + useAdminDeleteUser, + useAdminUpdateUser, + useAdminUsers, +} from '@/api/hooks' +import type { UserResponse } from '@/api/types' +import { UserRole } from '@/api/types' +import { PAGINATION } from '@/config' +import styles from './admin.module.scss' + +type ModalState = + | { type: 'closed' } + | { type: 'create' } + | { type: 'edit'; user: UserResponse } + | { type: 'delete'; user: UserResponse } + +export function Component(): React.ReactElement { + const [page, setPage] = useState(PAGINATION.DEFAULT_PAGE) + const [modal, setModal] = useState({ type: 'closed' }) + + const { data, isLoading } = useAdminUsers({ + page, + size: PAGINATION.DEFAULT_SIZE, + }) + const createUser = useAdminCreateUser() + const updateUser = useAdminUpdateUser() + const deleteUser = useAdminDeleteUser() + + const handleCreate = (formData: FormData): void => { + const email = formData.get('email') as string + const password = formData.get('password') as string + const fullName = (formData.get('fullName') as string) || undefined + const role = formData.get('role') as UserRole + const isActive = formData.get('isActive') === 'on' + + createUser.mutate( + { email, password, full_name: fullName, role, is_active: isActive }, + { onSuccess: () => setModal({ type: 'closed' }) } + ) + } + + const handleUpdate = (userId: string, formData: FormData): void => { + const email = formData.get('email') as string + const fullName = (formData.get('fullName') as string) || undefined + const role = formData.get('role') as UserRole + const isActive = formData.get('isActive') === 'on' + + updateUser.mutate( + { + id: userId, + data: { email, full_name: fullName, role, is_active: isActive }, + }, + { onSuccess: () => setModal({ type: 'closed' }) } + ) + } + + const handleDelete = (userId: string): void => { + deleteUser.mutate(userId, { onSuccess: () => setModal({ type: 'closed' }) }) + } + + const totalPages = data ? Math.ceil(data.total / PAGINATION.DEFAULT_SIZE) : 0 + + return ( +
+
+

Users

+ +
+ +
+
+
Email
+
Name
+
Role
+
Status
+
Actions
+
+ +
+ {isLoading &&
Loading...
} + + {!isLoading && data?.items.length === 0 && ( +
No users found
+ )} + + {data?.items.map((user) => ( +
+
+ {user.email} +
+
+ {user.full_name ?? '—'} +
+
+ + {user.role} + +
+
+ + {user.is_active ? 'Active' : 'Inactive'} + +
+
+ + +
+
+ ))} +
+ + {data && data.total > PAGINATION.DEFAULT_SIZE && ( +
+ + Page {page} of {totalPages} ({data.total} users) + +
+ + +
+
+ )} +
+ + {modal.type === 'create' && ( +
+ +
+
{ + e.preventDefault() + handleCreate(new FormData(e.currentTarget)) + }} + > +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ + )} + + {modal.type === 'edit' && ( +
+ +
+
{ + e.preventDefault() + handleUpdate(modal.user.id, new FormData(e.currentTarget)) + }} + > +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + + )} + + {modal.type === 'delete' && ( +
+ +
+
+

+ Are you sure you want to delete{' '} + {modal.user.email}? + This action cannot be undone. +

+
+ + +
+
+ + + )} + + ) +} + +Component.displayName = 'AdminUsers' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/index.tsx b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/index.tsx new file mode 100644 index 00000000..c2350435 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/index.tsx @@ -0,0 +1,313 @@ +/** + * AngelaMos | 2026 + * index.tsx + */ + +import { useState } from 'react' +import { LuPencil, LuTrash2, LuX } from 'react-icons/lu' +import { + useAdminDeleteProgram, + useAdminPrograms, + useAdminUpdateProgram, +} from '@/api/hooks' +import { + type AdminProgramResponse, + type ProgramStatus, + ProgramVisibility, + STATUS_LABELS, +} from '@/api/types' +import { PAGINATION } from '@/config' +import styles from './programs.module.scss' + +type ModalState = + | { type: 'closed' } + | { type: 'edit'; program: AdminProgramResponse } + | { type: 'delete'; program: AdminProgramResponse } + +export function Component(): React.ReactElement { + const [page, setPage] = useState(PAGINATION.DEFAULT_PAGE) + const [statusFilter, setStatusFilter] = useState('') + const [modal, setModal] = useState({ type: 'closed' }) + + const { data, isLoading } = useAdminPrograms({ + page, + size: PAGINATION.DEFAULT_SIZE, + status: statusFilter || undefined, + }) + const updateProgram = useAdminUpdateProgram() + const deleteProgram = useAdminDeleteProgram() + + const handleUpdate = (programId: string, formData: FormData): void => { + const status = formData.get('status') as ProgramStatus + const visibility = formData.get('visibility') as ProgramVisibility + + updateProgram.mutate( + { id: programId, data: { status, visibility } }, + { onSuccess: () => setModal({ type: 'closed' }) } + ) + } + + const handleDelete = (programId: string): void => { + deleteProgram.mutate(programId, { + onSuccess: () => setModal({ type: 'closed' }), + }) + } + + const totalPages = data ? Math.ceil(data.total / PAGINATION.DEFAULT_SIZE) : 0 + + return ( +
+
+

Programs

+ +
+ +
+
+
Program
+
Company
+
Status
+
Visibility
+
Reports
+
Actions
+
+ +
+ {isLoading &&
Loading...
} + + {!isLoading && data?.items.length === 0 && ( +
No programs found
+ )} + + {data?.items.map((program) => ( +
+
+
+ {program.name} + /{program.slug} +
+
+
+
+ + {program.company_name ?? 'N/A'} + + + {program.company_email} + +
+
+
+ + {STATUS_LABELS[program.status]} + +
+
+ {program.visibility} +
+
+ {program.report_count} +
+
+ + +
+
+ ))} +
+ + {data && data.total > PAGINATION.DEFAULT_SIZE && ( +
+ + Page {page} of {totalPages} ({data.total} programs) + +
+ + +
+
+ )} +
+ + {modal.type === 'edit' && ( +
+ +
+
{ + e.preventDefault() + handleUpdate(modal.program.id, new FormData(e.currentTarget)) + }} + > +
+ {modal.program.name} + + by {modal.program.company_name ?? modal.program.company_email} + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + )} + + {modal.type === 'delete' && ( +
+ +
+
+

+ Are you sure you want to delete{' '} + {modal.program.name}? + This will permanently remove the program and all associated data. +

+
+ + +
+
+ + + )} + + ) +} + +Component.displayName = 'AdminPrograms' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/programs.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/programs.module.scss new file mode 100644 index 00000000..596705a3 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/programs/programs.module.scss @@ -0,0 +1,480 @@ +// =================== +// AngelaMos | 2026 +// programs.module.scss +// =================== + +@use '@/styles' as *; + +.page { + padding: $space-6; + min-height: calc(100vh - 56px); +} + +.header { + @include flex-between; + margin-bottom: $space-6; +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.filterSelect { + height: 40px; + padding: 0 $space-3; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + cursor: pointer; + @include transition-fast; + + &:focus { + outline: none; + border-color: $border-strong; + } + + option { + background: $bg-surface-100; + color: $text-default; + } +} + +.table { + width: 100%; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + overflow: hidden; +} + +.tableHeader { + display: grid; + grid-template-columns: 2fr 1.5fr 1fr 1fr 0.5fr 100px; + gap: $space-4; + padding: $space-3 $space-4; + background: $bg-surface-200; + border-bottom: 1px solid $border-default; + + @include breakpoint-down('lg') { + display: none; + } +} + +.tableHeaderCell { + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wide; +} + +.tableBody { + @include flex-column; +} + +.tableRow { + display: grid; + grid-template-columns: 2fr 1.5fr 1fr 1fr 0.5fr 100px; + gap: $space-4; + padding: $space-3 $space-4; + border-bottom: 1px solid $border-default; + @include transition-fast; + + &:last-child { + border-bottom: none; + } + + @include hover { + background: $bg-surface-75; + } + + @include breakpoint-down('lg') { + grid-template-columns: 1fr; + gap: $space-2; + } +} + +.tableCell { + display: flex; + align-items: center; + font-size: $font-size-sm; + color: $text-default; + min-width: 0; + + @include breakpoint-down('lg') { + &::before { + content: attr(data-label); + font-size: $font-size-xs; + color: $text-lighter; + margin-right: $space-2; + min-width: 80px; + } + } +} + +.programInfo { + @include flex-column; + gap: $space-1; + min-width: 0; +} + +.programName { + font-weight: $font-weight-medium; + color: $text-default; + @include truncate; +} + +.programSlug { + font-size: $font-size-xs; + color: $text-lighter; + @include truncate; +} + +.companyInfo { + @include flex-column; + gap: $space-1; + min-width: 0; +} + +.companyName { + color: $text-default; + @include truncate; +} + +.companyEmail { + font-size: $font-size-xs; + color: $text-lighter; + @include truncate; +} + +.badge { + display: inline-flex; + align-items: center; + padding: $space-1 $space-2; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &.draft { + background: $bg-surface-200; + color: $text-light; + } + + &.active { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &.paused { + background: hsl(45 93% 47% / 20%); + color: hsl(45, 93%, 57%); + } + + &.closed { + background: hsl(0 72% 51% / 20%); + color: $error-light; + } +} + +.visibility { + font-size: $font-size-sm; + color: $text-light; + text-transform: capitalize; +} + +.reportCount { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $accent-orange; +} + +.actions { + display: flex; + gap: $space-2; + justify-content: flex-end; + + @include breakpoint-down('lg') { + justify-content: flex-start; + } +} + +.actionBtn { + width: 32px; + height: 32px; + @include flex-center; + border: 1px solid $border-default; + border-radius: $radius-md; + background: transparent; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &.delete { + @include hover { + border-color: $error-default; + color: $error-default; + } + } + + svg { + width: 16px; + height: 16px; + } +} + +.pagination { + @include flex-between; + padding: $space-4; + border-top: 1px solid $border-default; +} + +.paginationInfo { + font-size: $font-size-sm; + color: $text-lighter; +} + +.paginationBtns { + display: flex; + gap: $space-2; +} + +.paginationBtn { + padding: $space-2 $space-3; + border: 1px solid $border-default; + border-radius: $radius-md; + background: transparent; + font-size: $font-size-sm; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.empty { + @include flex-column-center; + padding: $space-12; + color: $text-muted; + font-size: $font-size-sm; +} + +.loading { + @include flex-center; + padding: $space-12; + color: $text-muted; +} + +.modal { + position: fixed; + inset: 0; + z-index: $z-modal; + @include flex-center; +} + +.modalOverlay { + @include absolute-fill; + background: rgb(0, 0, 0, 70%); +} + +.modalContent { + position: relative; + width: 100%; + max-width: 400px; + margin: $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + padding: $space-6; +} + +.modalHeader { + @include flex-between; + margin-bottom: $space-5; +} + +.modalTitle { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.modalClose { + width: 32px; + height: 32px; + @include flex-center; + border: none; + border-radius: $radius-md; + background: transparent; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + svg { + width: 20px; + height: 20px; + } +} + +.form { + @include flex-column; + gap: $space-4; +} + +.programLabel { + @include flex-column; + gap: $space-1; + font-size: $font-size-base; + font-weight: $font-weight-medium; + color: $text-default; + padding-bottom: $space-3; + border-bottom: 1px solid $border-default; +} + +.programSubLabel { + font-size: $font-size-sm; + font-weight: $font-weight-regular; + color: $text-lighter; +} + +.field { + @include flex-column; + gap: $space-2; +} + +.label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; +} + +.select { + width: 100%; + height: 44px; + padding: 0 $space-3; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + cursor: pointer; + @include transition-fast; + + &:focus { + outline: none; + border-color: $border-strong; + } + + option { + background: $bg-surface-100; + color: $text-default; + } +} + +.formActions { + display: flex; + gap: $space-3; + margin-top: $space-2; +} + +.submitBtn { + flex: 1; + height: 44px; + @include flex-center; + background: $white; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $black; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(0.9); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.cancelBtn { + flex: 1; + height: 44px; + @include flex-center; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } +} + +.deleteConfirm { + @include flex-column; + gap: $space-4; +} + +.deleteText { + font-size: $font-size-sm; + color: $text-light; + line-height: $line-height-relaxed; +} + +.deleteName { + font-weight: $font-weight-medium; + color: $text-default; +} + +.deleteBtn { + flex: 1; + height: 44px; + @include flex-center; + background: $error-default; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $white; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(0.9); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/index.tsx b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/index.tsx new file mode 100644 index 00000000..3c2e3823 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/index.tsx @@ -0,0 +1,321 @@ +/** + * AngelaMos | 2026 + * index.tsx + */ + +import { useState } from 'react' +import { LuPencil, LuX } from 'react-icons/lu' +import { useAdminReports, useAdminUpdateReport } from '@/api/hooks' +import { + type AdminReportResponse, + type AdminReportUpdate, + REPORT_STATUS_LABELS, + type ReportStatus, + SEVERITY_LABELS, + type Severity, +} from '@/api/types' +import { PAGINATION } from '@/config' +import styles from './reports.module.scss' + +type ModalState = + | { type: 'closed' } + | { type: 'edit'; report: AdminReportResponse } + +export function Component(): React.ReactElement { + const [page, setPage] = useState(PAGINATION.DEFAULT_PAGE) + const [statusFilter, setStatusFilter] = useState('') + const [severityFilter, setSeverityFilter] = useState('') + const [modal, setModal] = useState({ type: 'closed' }) + + const { data, isLoading } = useAdminReports({ + page, + size: PAGINATION.DEFAULT_SIZE, + status: statusFilter || undefined, + severity: severityFilter || undefined, + }) + const updateReport = useAdminUpdateReport() + + const handleUpdate = (reportId: string, formData: FormData): void => { + const updateData: AdminReportUpdate = {} + + const status = formData.get('status') as ReportStatus + if (status) updateData.status = status + + const severityFinal = formData.get('severity_final') as Severity + if (severityFinal) updateData.severity_final = severityFinal + + const bountyAmountStr = formData.get('bounty_amount') as string + if (bountyAmountStr) { + updateData.bounty_amount = Math.round(parseFloat(bountyAmountStr) * 100) + } + + updateReport.mutate( + { id: reportId, data: updateData }, + { onSuccess: () => setModal({ type: 'closed' }) } + ) + } + + const formatCurrency = (cents: number | null): string => { + if (cents === null) return '-' + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + }).format(cents / 100) + } + + const toCamelCase = (str: string): string => + str.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase()) + + const totalPages = data ? Math.ceil(data.total / PAGINATION.DEFAULT_SIZE) : 0 + + return ( +
+
+

Reports

+
+ + +
+
+ +
+
+
Report
+
Program
+
Researcher
+
Severity
+
Status
+
Bounty
+
Actions
+
+ +
+ {isLoading &&
Loading...
} + + {!isLoading && data?.items.length === 0 && ( +
No reports found
+ )} + + {data?.items.map((report) => ( +
+
+ {report.title} +
+
+ {report.program_name} +
+
+
+ + {report.researcher_name ?? 'Anonymous'} + + + {report.researcher_email} + +
+
+
+ + { + SEVERITY_LABELS[ + report.severity_final ?? report.severity_submitted + ] + } + +
+
+ + {REPORT_STATUS_LABELS[report.status]} + +
+
+ + {formatCurrency(report.bounty_amount)} + +
+
+ +
+
+ ))} +
+ + {data && data.total > PAGINATION.DEFAULT_SIZE && ( +
+ + Page {page} of {totalPages} ({data.total} reports) + +
+ + +
+
+ )} +
+ + {modal.type === 'edit' && ( +
+ +
+
{ + e.preventDefault() + handleUpdate(modal.report.id, new FormData(e.currentTarget)) + }} + > +
+ {modal.report.title} + + {modal.report.program_name} - {modal.report.researcher_email} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + )} + + ) +} + +Component.displayName = 'AdminReports' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/reports.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/reports.module.scss new file mode 100644 index 00000000..aa128757 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/reports/reports.module.scss @@ -0,0 +1,497 @@ +// =================== +// AngelaMos | 2026 +// reports.module.scss +// =================== + +@use '@/styles' as *; + +.page { + padding: $space-6; + min-height: calc(100vh - 56px); +} + +.header { + @include flex-between; + flex-wrap: wrap; + gap: $space-4; + margin-bottom: $space-6; +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.filters { + display: flex; + gap: $space-3; +} + +.filterSelect { + height: 40px; + padding: 0 $space-3; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + cursor: pointer; + @include transition-fast; + + &:focus { + outline: none; + border-color: $border-strong; + } + + option { + background: $bg-surface-100; + color: $text-default; + } +} + +.table { + width: 100%; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + overflow: hidden; +} + +.tableHeader { + display: grid; + grid-template-columns: 2fr 1fr 1.5fr 0.8fr 0.8fr 0.8fr 80px; + gap: $space-4; + padding: $space-3 $space-4; + background: $bg-surface-200; + border-bottom: 1px solid $border-default; + + @include breakpoint-down('xl') { + display: none; + } +} + +.tableHeaderCell { + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wide; +} + +.tableBody { + @include flex-column; +} + +.tableRow { + display: grid; + grid-template-columns: 2fr 1fr 1.5fr 0.8fr 0.8fr 0.8fr 80px; + gap: $space-4; + padding: $space-3 $space-4; + border-bottom: 1px solid $border-default; + @include transition-fast; + + &:last-child { + border-bottom: none; + } + + @include hover { + background: $bg-surface-75; + } + + @include breakpoint-down('xl') { + grid-template-columns: 1fr; + gap: $space-2; + } +} + +.tableCell { + display: flex; + align-items: center; + font-size: $font-size-sm; + color: $text-default; + min-width: 0; + + @include breakpoint-down('xl') { + &::before { + content: attr(data-label); + font-size: $font-size-xs; + color: $text-lighter; + margin-right: $space-2; + min-width: 80px; + } + } +} + +.reportTitle { + @include truncate; + font-weight: $font-weight-medium; +} + +.programName { + @include truncate; + color: $text-light; +} + +.researcherInfo { + @include flex-column; + gap: $space-1; + min-width: 0; +} + +.researcherName { + @include truncate; +} + +.researcherEmail { + font-size: $font-size-xs; + color: $text-lighter; + @include truncate; +} + +.severityBadge { + display: inline-flex; + align-items: center; + padding: $space-1 $space-2; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &.critical { + background: hsl(0 72% 51% / 20%); + color: hsl(0, 72%, 65%); + } + + &.high { + background: hsl(25 95% 53% / 20%); + color: hsl(25, 95%, 63%); + } + + &.medium { + background: hsl(45 93% 47% / 20%); + color: hsl(45, 93%, 57%); + } + + &.low { + background: hsl(217 91% 60% / 20%); + color: hsl(217, 91%, 70%); + } + + &.informational { + background: $bg-surface-200; + color: $text-light; + } +} + +.statusBadge { + display: inline-flex; + align-items: center; + padding: $space-1 $space-2; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &.new { + background: hsl(217 91% 60% / 20%); + color: hsl(217, 91%, 70%); + } + + &.triaging { + background: hsl(45 93% 47% / 20%); + color: hsl(45, 93%, 57%); + } + + &.needsMoreInfo { + background: hsl(280 65% 60% / 20%); + color: hsl(280, 65%, 70%); + } + + &.accepted { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &.duplicate, + &.informative, + &.notApplicable { + background: $bg-surface-200; + color: $text-light; + } + + &.resolved { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &.disclosed { + background: hsl(160 60% 45% / 20%); + color: hsl(160, 60%, 55%); + } +} + +.bountyAmount { + font-weight: $font-weight-medium; + color: $accent-orange; +} + +.actions { + display: flex; + gap: $space-2; + justify-content: flex-end; + + @include breakpoint-down('xl') { + justify-content: flex-start; + } +} + +.actionBtn { + width: 32px; + height: 32px; + @include flex-center; + border: 1px solid $border-default; + border-radius: $radius-md; + background: transparent; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + svg { + width: 16px; + height: 16px; + } +} + +.pagination { + @include flex-between; + padding: $space-4; + border-top: 1px solid $border-default; +} + +.paginationInfo { + font-size: $font-size-sm; + color: $text-lighter; +} + +.paginationBtns { + display: flex; + gap: $space-2; +} + +.paginationBtn { + padding: $space-2 $space-3; + border: 1px solid $border-default; + border-radius: $radius-md; + background: transparent; + font-size: $font-size-sm; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.empty { + @include flex-column-center; + padding: $space-12; + color: $text-muted; + font-size: $font-size-sm; +} + +.loading { + @include flex-center; + padding: $space-12; + color: $text-muted; +} + +.modal { + position: fixed; + inset: 0; + z-index: $z-modal; + @include flex-center; +} + +.modalOverlay { + @include absolute-fill; + background: rgb(0, 0, 0, 70%); +} + +.modalContent { + position: relative; + width: 100%; + max-width: 400px; + margin: $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + padding: $space-6; +} + +.modalHeader { + @include flex-between; + margin-bottom: $space-5; +} + +.modalTitle { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.modalClose { + width: 32px; + height: 32px; + @include flex-center; + border: none; + border-radius: $radius-md; + background: transparent; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } + + svg { + width: 20px; + height: 20px; + } +} + +.form { + @include flex-column; + gap: $space-4; +} + +.reportLabel { + @include flex-column; + gap: $space-1; + font-size: $font-size-base; + font-weight: $font-weight-medium; + color: $text-default; + padding-bottom: $space-3; + border-bottom: 1px solid $border-default; +} + +.reportSubLabel { + font-size: $font-size-sm; + font-weight: $font-weight-regular; + color: $text-lighter; +} + +.field { + @include flex-column; + gap: $space-2; +} + +.label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; +} + +.select { + width: 100%; + height: 44px; + padding: 0 $space-3; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + cursor: pointer; + @include transition-fast; + + &:focus { + outline: none; + border-color: $border-strong; + } + + option { + background: $bg-surface-100; + color: $text-default; + } +} + +.input { + width: 100%; + height: 44px; + padding: 0 $space-3; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + @include transition-fast; + + &::placeholder { + color: $text-muted; + } + + &:focus { + outline: none; + border-color: $border-strong; + } +} + +.formActions { + display: flex; + gap: $space-3; + margin-top: $space-2; +} + +.submitBtn { + flex: 1; + height: 44px; + @include flex-center; + background: $white; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $black; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(0.9); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.cancelBtn { + flex: 1; + height: 44px; + @include flex-center; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/index.tsx b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/index.tsx new file mode 100644 index 00000000..0ff2bf4b --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/index.tsx @@ -0,0 +1,104 @@ +/** + * AngelaMos | 2026 + * index.tsx + */ + +import { usePlatformStats } from '@/api/hooks' +import styles from './stats.module.scss' + +export function Component(): React.ReactElement { + const { data: stats, isLoading } = usePlatformStats() + + if (isLoading) { + return ( +
+
Loading stats...
+
+ ) + } + + if (!stats) { + return ( +
+
Failed to load platform statistics
+
+ ) + } + + const formatCurrency = (cents: number): string => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + }).format(cents / 100) + } + + return ( +
+
+

Platform Statistics

+
+ +
+
+ Total Users + {stats.total_users} +
+
+ Researchers + {stats.total_researchers} +
+
+ Companies + {stats.total_companies} +
+
+ New Users (30d) + {stats.new_users_this_month} +
+
+ +
+
+ Total Programs + {stats.total_programs} +
+
+ Active Programs + + {stats.active_programs} + +
+
+ Total Reports + {stats.total_reports} +
+
+ Reports (30d) + {stats.reports_this_month} +
+
+ +
+

Total Bounties Paid

+
+ {formatCurrency(stats.total_bounties_paid)} +
+
+ +
+

Reports by Status

+
+ {Object.entries(stats.reports_by_status).map(([status, count]) => ( +
+ {status} + {count} +
+ ))} +
+
+
+ ) +} + +Component.displayName = 'AdminStats' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/stats.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/stats.module.scss new file mode 100644 index 00000000..94eea304 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/admin/stats/stats.module.scss @@ -0,0 +1,115 @@ +// =================== +// AngelaMos | 2026 +// stats.module.scss +// =================== + +@use '@/styles' as *; + +.page { + padding: $space-6; + min-height: calc(100vh - 56px); +} + +.header { + margin-bottom: $space-6; +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.loading, +.error { + @include flex-center; + padding: $space-12; + color: $text-muted; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: $space-4; + margin-bottom: $space-6; + + @include breakpoint-down('lg') { + grid-template-columns: repeat(2, 1fr); + } + + @include breakpoint-down('sm') { + grid-template-columns: 1fr; + } +} + +.statCard { + @include flex-column; + gap: $space-2; + padding: $space-5; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; +} + +.statLabel { + font-size: $font-size-sm; + color: $text-lighter; +} + +.statValue { + font-size: $font-size-3xl; + font-weight: $font-weight-semibold; + color: $text-default; + + &.highlight { + color: $accent-orange; + } +} + +.section { + margin-bottom: $space-6; +} + +.sectionTitle { + font-size: $font-size-lg; + font-weight: $font-weight-semibold; + color: $text-default; + margin-bottom: $space-4; +} + +.bountyTotal { + font-size: $font-size-4xl; + font-weight: $font-weight-semibold; + color: $accent-orange; + padding: $space-6; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + text-align: center; +} + +.statusGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: $space-3; +} + +.statusItem { + @include flex-between; + padding: $space-3 $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-md; +} + +.statusLabel { + font-size: $font-size-sm; + color: $text-light; + text-transform: capitalize; +} + +.statusCount { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-default; +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/inbox.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/inbox.module.scss new file mode 100644 index 00000000..e4832e5a --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/inbox.module.scss @@ -0,0 +1,254 @@ +// =================== +// AngelaMos | 2026 +// inbox.module.scss +// =================== + +@use '@/styles' as *; + +.page { + min-height: calc(100vh - 56px); + padding: $space-6; + background-color: $bg-default; +} + +.container { + max-width: 1000px; + margin: 0 auto; +} + +.header { + @include flex-between; + margin-bottom: $space-6; + gap: $space-4; + flex-wrap: wrap; +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; + margin-bottom: $space-2; +} + +.subtitle { + font-size: $font-size-sm; + color: $text-lighter; +} + +.stats { + display: flex; + align-items: baseline; + gap: $space-2; + padding: $space-3 $space-4; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-md; +} + +.statValue { + font-size: $font-size-xl; + font-weight: $font-weight-semibold; + color: $accent-orange; +} + +.statLabel { + font-size: $font-size-sm; + color: $text-lighter; +} + +.table { + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + overflow: hidden; +} + +.tableHeader { + display: grid; + grid-template-columns: 2fr 1fr 1fr 120px; + gap: $space-4; + padding: $space-3 $space-4; + background: $bg-surface-200; + border-bottom: 1px solid $border-default; + + @include breakpoint-down('md') { + display: none; + } +} + +.headerCell { + font-size: $font-size-xs; + font-weight: $font-weight-medium; + color: $text-lighter; + text-transform: uppercase; + letter-spacing: $tracking-wide; +} + +.tableBody { + @include flex-column; +} + +.row { + display: grid; + grid-template-columns: 2fr 1fr 1fr 120px; + gap: $space-4; + padding: $space-4; + border-bottom: 1px solid $border-muted; + text-decoration: none; + @include transition-fast; + + &:last-child { + border-bottom: none; + } + + @include hover { + background: $bg-surface-75; + } + + @include breakpoint-down('md') { + grid-template-columns: 1fr; + gap: $space-3; + } +} + +.cell { + display: flex; + align-items: center; + min-width: 0; + + @include breakpoint-down('md') { + &:first-child { + flex-direction: column; + align-items: flex-start; + } + } +} + +.reportTitle { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; + @include truncate; +} + +.status { + display: inline-flex; + padding: $space-1 $space-2; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &[data-status='new'] { + background: hsl(217 91% 60% / 20%); + color: hsl(217, 91%, 70%); + } + + &[data-status='triaging'] { + background: hsl(38 92% 50% / 20%); + color: hsl(38, 92%, 60%); + } + + &[data-status='needs_more_info'] { + background: hsl(263 70% 50% / 20%); + color: hsl(263, 70%, 70%); + } + + &[data-status='accepted'] { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &[data-status='duplicate'], + &[data-status='not_applicable'] { + background: $bg-surface-200; + color: $text-lighter; + } + + &[data-status='informative'] { + background: hsl(188 78% 41% / 20%); + color: hsl(188, 78%, 51%); + } + + &[data-status='resolved'] { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &[data-status='disclosed'] { + background: hsl(160 84% 39% / 20%); + color: hsl(160, 84%, 49%); + } +} + +.severity { + display: inline-flex; + padding: $space-1 $space-2; + border-radius: $radius-sm; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &[data-severity='critical'] { + background: hsl(0 72% 51% / 20%); + color: hsl(0, 72%, 60%); + } + + &[data-severity='high'] { + background: hsl(24 95% 53% / 20%); + color: hsl(24, 95%, 63%); + } + + &[data-severity='medium'] { + background: hsl(45 93% 47% / 20%); + color: hsl(45, 93%, 57%); + } + + &[data-severity='low'] { + background: hsl(217 91% 60% / 20%); + color: hsl(217, 91%, 70%); + } + + &[data-severity='informational'] { + background: $bg-surface-200; + color: $text-lighter; + } +} + +.date { + font-size: $font-size-sm; + color: $text-light; +} + +.loading { + @include flex-center; + padding: $space-12; + color: $text-muted; + font-size: $font-size-sm; +} + +.error { + @include flex-center; + padding: $space-12; + color: $error-default; + font-size: $font-size-sm; +} + +.empty { + @include flex-column-center; + padding: $space-12; + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + gap: $space-2; + + p { + color: $text-light; + font-size: $font-size-sm; + } +} + +.emptyHint { + color: $text-muted; + font-size: $font-size-xs; + text-align: center; + max-width: 320px; +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/index.tsx b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/index.tsx new file mode 100644 index 00000000..7c4c4741 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/inbox/index.tsx @@ -0,0 +1,115 @@ +/** + * AngelaMos | 2026 + * index.tsx + */ + +import { Link } from 'react-router-dom' +import { useInbox } from '@/api/hooks' +import { REPORT_STATUS_LABELS, type Report, SEVERITY_LABELS } from '@/api/types' +import { ROUTES } from '@/config' +import styles from './inbox.module.scss' + +function formatDate(dateString: string): string { + return new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }).format(new Date(dateString)) +} + +function ReportRow({ report }: { report: Report }): React.ReactElement { + return ( + +
+ {report.title} +
+
+ + {REPORT_STATUS_LABELS[report.status]} + +
+
+ + {SEVERITY_LABELS[report.severity_submitted]} + +
+
+ {formatDate(report.created_at)} +
+ + ) +} + +export function Component(): React.ReactElement { + const { data, isLoading, error } = useInbox(1, 50) + + if (isLoading) { + return ( +
+
+
Loading inbox...
+
+
+ ) + } + + if (error) { + return ( +
+
+
Failed to load inbox
+
+
+ ) + } + + const reports = data?.items ?? [] + + return ( +
+
+
+
+

Inbox

+

+ Review and triage incoming vulnerability reports +

+
+
+ {data?.total ?? 0} + total reports +
+
+ + {reports.length === 0 ? ( +
+

No reports in your inbox

+ + Reports will appear here when researchers submit vulnerabilities to + your programs + +
+ ) : ( +
+
+ Report + Status + Severity + Submitted +
+
+ {reports.map((report) => ( + + ))} +
+
+ )} +
+
+ ) +} + +Component.displayName = 'CompanyInbox' diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.module.scss b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.module.scss new file mode 100644 index 00000000..f3fef408 --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.module.scss @@ -0,0 +1,375 @@ +// =================== +// AngelaMos | 2026 +// edit.module.scss +// =================== + +@use '@/styles' as *; + +.page { + min-height: calc(100vh - 56px); + padding: $space-6; + background-color: $bg-default; +} + +.container { + max-width: 900px; + margin: 0 auto; +} + +.header { + margin-bottom: $space-6; +} + +.backLink { + display: inline-flex; + align-items: center; + gap: $space-2; + font-size: $font-size-sm; + color: $text-light; + margin-bottom: $space-4; + @include transition-fast; + + @include hover { + color: $text-default; + } +} + +.title { + font-size: $font-size-2xl; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.section { + background: $bg-surface-100; + border: 1px solid $border-default; + border-radius: $radius-lg; + padding: $space-5; + margin-bottom: $space-6; +} + +.sectionHeader { + @include flex-between; + margin-bottom: $space-4; +} + +.sectionTitle { + font-size: $font-size-base; + font-weight: $font-weight-semibold; + color: $text-default; +} + +.fields { + @include flex-column; + gap: $space-4; +} + +.field { + @include flex-column; + gap: $space-2; +} + +.label { + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-light; +} + +.input { + width: 100%; + height: 40px; + padding: 0 $space-3; + background: $bg-control; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + @include transition-fast; + + &:focus { + outline: none; + border-color: $border-strong; + } + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &[type='number'] { + -moz-appearance: textfield; + } +} + +.select { + width: 100%; + height: 40px; + padding: 0 $space-3; + background: $bg-control; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + cursor: pointer; + + &:focus { + outline: none; + border-color: $border-strong; + } + + option { + background: $bg-surface-100; + } +} + +.textarea { + width: 100%; + padding: $space-3; + background: $bg-control; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + resize: vertical; + min-height: 80px; + font-family: inherit; + + &:focus { + outline: none; + border-color: $border-strong; + } +} + +.row { + display: grid; + gap: $space-4; + + @include breakpoint-up('md') { + grid-template-columns: repeat(3, 1fr); + } +} + +.checkLabel { + display: flex; + align-items: center; + gap: $space-2; + font-size: $font-size-sm; + color: $text-light; + cursor: pointer; + + input { + width: 16px; + height: 16px; + accent-color: $accent-orange; + } +} + +.saveBtn { + align-self: flex-start; + height: 40px; + padding: 0 $space-5; + background: $accent-orange; + border: none; + border-radius: $radius-md; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $white; + cursor: pointer; + @include transition-fast; + + @include hover { + filter: brightness(1.1); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.addBtn { + height: 36px; + padding: 0 $space-4; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + background: $bg-surface-200; + color: $text-default; + } +} + +.addForm { + @include flex-column; + gap: $space-4; + padding: $space-4; + background: $bg-surface-75; + border: 1px solid $border-muted; + border-radius: $radius-md; + margin-bottom: $space-4; +} + +.assetList { + @include flex-column; + gap: $space-2; +} + +.assetItem { + @include flex-between; + padding: $space-3; + background: $bg-surface-75; + border: 1px solid $border-muted; + border-radius: $radius-md; +} + +.assetInfo { + display: flex; + align-items: center; + gap: $space-3; + flex-wrap: wrap; +} + +.assetType { + padding: $space-0-5 $space-2; + background: $bg-surface-300; + border-radius: $radius-sm; + font-size: $font-size-xs; + color: $text-lighter; +} + +.assetId { + font-size: $font-size-sm; + font-family: $font-mono; + color: $text-default; +} + +.scopeBadge { + padding: $space-0-5 $space-2; + border-radius: $radius-full; + font-size: $font-size-xs; + font-weight: $font-weight-medium; + + &[data-scope='in'] { + background: hsl(142 76% 36% / 20%); + color: hsl(142, 76%, 46%); + } + + &[data-scope='out'] { + background: hsl(0 72% 51% / 20%); + color: $error-light; + } +} + +.deleteBtn { + padding: $space-1 $space-3; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-xs; + color: $text-light; + cursor: pointer; + @include transition-fast; + + @include hover { + border-color: $error-default; + color: $error-default; + } +} + +.emptyText { + font-size: $font-size-sm; + color: $text-muted; + padding: $space-4; + text-align: center; +} + +.rewardGrid { + @include flex-column; + gap: $space-3; + margin-bottom: $space-4; +} + +.rewardRow { + display: flex; + align-items: center; + gap: $space-4; + padding: $space-3; + background: $bg-surface-75; + border: 1px solid $border-muted; + border-radius: $radius-md; + + @include breakpoint-down('sm') { + flex-direction: column; + align-items: flex-start; + } +} + +.severityLabel { + width: 120px; + font-size: $font-size-sm; + font-weight: $font-weight-medium; + color: $text-default; + flex-shrink: 0; +} + +.rewardInputs { + display: flex; + align-items: center; + gap: $space-2; +} + +.currency { + font-size: $font-size-sm; + color: $text-muted; +} + +.rewardInput { + width: 100px; + height: 36px; + padding: 0 $space-2; + background: $bg-control; + border: 1px solid $border-default; + border-radius: $radius-md; + font-size: $font-size-sm; + color: $text-default; + text-align: right; + + &:focus { + outline: none; + border-color: $border-strong; + } + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &[type='number'] { + -moz-appearance: textfield; + } +} + +.rewardSep { + color: $text-muted; +} + +.loading { + @include flex-center; + padding: $space-12; + color: $text-muted; + font-size: $font-size-sm; +} + +.error { + @include flex-center; + padding: $space-12; + color: $error-default; + font-size: $font-size-sm; +} diff --git a/PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.tsx b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.tsx new file mode 100644 index 00000000..0d23917f --- /dev/null +++ b/PROJECTS/bug-bounty-platform/frontend/src/routes/company/programs/[id]/edit.tsx @@ -0,0 +1,439 @@ +/** + * AngelaMos | 2026 + * edit.tsx + */ + +import { useEffect, useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import { + useAddAsset, + useDeleteAsset, + useProgram, + useSetRewardTiers, + useUpdateProgram, +} from '@/api/hooks' +import { + ASSET_TYPE_LABELS, + type Asset, + AssetType, + type ProgramDetail, + type ProgramStatus, + ProgramVisibility, + SEVERITY_LABELS, + Severity, + STATUS_LABELS, +} from '@/api/types' +import { ROUTES } from '@/config' +import styles from './edit.module.scss' + +function ProgramDetails({ + program, +}: { + program: ProgramDetail +}): React.ReactElement { + const updateProgram = useUpdateProgram() + const [name, setName] = useState(program.name) + const [description, setDescription] = useState(program.description ?? '') + const [rules, setRules] = useState(program.rules ?? '') + const [sla, setSla] = useState(program.response_sla_hours) + const [status, setStatus] = useState(program.status) + const [visibility, setVisibility] = useState(program.visibility) + + const handleSave = async (): Promise => { + await updateProgram.mutateAsync({ + id: program.id, + data: { + name, + description: description || undefined, + rules: rules || undefined, + response_sla_hours: sla, + status, + visibility, + }, + }) + } + + return ( +
+

Program Details

+
+ +
+ + + +
+

sq@j4#83!c~m&ZwUgy-1jCS+~sK~ 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 literal 0 HcmV?d00001 diff --git a/PROJECTS/bug-bounty-platform/frontend/public/assets/favicon-32x32.png b/PROJECTS/bug-bounty-platform/frontend/public/assets/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..338e90d0fd359ba5da2ff517a1a4f32f0f40e78c GIT binary patch literal 2032 zcmV^_n??QOm%DfG4k{3jlarIEM=vEMC1hr1Qe0d-#B4Aa`0}6s7#jc3jtsz@Q01Z1 z=rRv;`}S>WYHCPLP36Lc3!FG{g7Wfm4*%|TLW+)?Q(=}iDu5|VW)PenJKJqF{bJ$KJ`nO=*ck#zwP7%n?TgAPFC;`#U8~CGyskr>BR;MTw{q=A-%YKis@|lfJ$_*_dxiDz?R^sCi;%WTL3d0w^pjBtJi& zBZm$V6dq1Qm?QTt9YLefFfcHHpPwJz-rjU~cVp4fj$QU2xvmYQKVmXx13V~>)Y2BM zC9u7f?iD2Wgk_Np0S?N(zi%YPe-Lv z(H0)bc`tvizO{+geR)jiXk$`!C2j-#bPmFpz{#f8#mrLyv{YB|@v4Bb@&p(dA#undcVKjfCqfZ!?q*`B1V|-AM)HQic z09-wLhTS`JSdsN19*&Om#b~g#wKWw~Cc2jR zl?B{WYtU@XGLbhd|#|K9N_{NPJJnyW+^AGRIyaR>-EG;eN{?jTG07j<9#zqEDe#L`9aC38`T^Osq zc9qNTq;ustZ>*mWrd0hC)Z5;dSE|fdgVzV^@p&^1=hs)#vwRf~vOmH4Mio*NHR^q` z9T7l9$!XLW7$dgv-Pc|vXWA5Q&z*(iUv}Wx(u^cHrM-n#0kEKVkalqt`_G@nr|~}e zsF_U9+=7LL1uelL`1<-jX?JKW=@}Xf@AO^zMmD0hw7~DH&zbtd3pnlEORtOTa5hu| z;6$;*7y99x@H*-Ack1;#eE1L>;X8Ieo%6##fnF%sy6*tRSaDRWE>?Jx{%Aff$@8gS zn9S{vu*X155+D~^TM#c^D?tgaEay^`7U$Smgl)(aGb5L$?!e~Fn~7PHOq#-uW74X` zHoMGvk*c+C!<`yzs;^NsK2Ut1krfBl)zJLerz0~wqL=`XBHaG{`%#EvrfhCv<5&e( zUQ8hVz^5pLBL*D(jFu^(IBncQg8=pdi zFMvgV-6MHqx)GVN~4a$4=|xhI^i&DM@KOC`4oPsucM zl?i~(&Q4k`e#4qF9T&&g(XuXs>gsAa;DjZoppJV9i+>l;aTl_wPFG^eV;8}H+C-VNJC4Ff z`WGZIZq;w;?d_Gsp8A>qu%8@E)vs6L>FGHnkei!Jc6K&v)~q2fFAtqg$DTcVsHmvG zH!u(hWLfz|W@E>4X{Ycms~XCOt>@Wh|V^dV(WbW8yFil`;6*hj>6Bd@)^Jplm$ z?AWn`y1F_hPMkhlgnoXAT@UN|sJe_h1E8$0L!{g|I5-$pjMyqhqzFQ1 z?k@DN{G6UsCvXrMkOWTJPD!z}`s?CCQiyz^DI$sw9o$J&I#OeRHmfldiex4xCPrv6 zU(5n9)ZL-ySU&osd5pJK5)l!>ty{NbWaMR&)t*B!i)93 zUBwIi5{Y3N9zkFi!Y}R;xw&5s~iuy?ggES7b`Va|P+zjk2;J z@7@CzFJ3%c)rftwJ75(Wj^oZ;q*eq1T3cJm%F05mRzIfD)zy`*t}dp!xzZ$#<-1$A z%DLiWOd|};0x-%9igdg=X&$Q5(=O~@9!^>367n~Zj}epF_kACRiick0{{U3{~W2|`2YX_21!IgR09B6Vn9z_E+-5C O0000f+2z|CH8pj$YHGT=YIHSK z`?b27T4yyiHB(dR->vo3)LiJ9rR6{GTdJu=8mg(aram-;Mo6F2MPY-2_U_#$eEReW zp`oF8^X3ig9d_Z#lP6_*K7ana%(G|DKVtgarzj{WfSsKko<4nwfrBRE?%jeieM^sj z{^Ap^#=jRd6A}_c|J2k}bn9k~Z#u8W+qZ8^%u#xLMD#oKANB|#Au-snVFR{r-;U)g z_oGFtZ=u&<24Z4j${nAUmL}%^`|rQuTJ zzksZ)EQE)LW9rnYc>dxwjvYIO))o$EVLlC=dmP0k=Q#KW-Yl9!p8usw9bsUyAAh<8 zD?1QeQj1mp`m>q-~SK+%WkRCak_cXs22Msv$-qJ16JbdpfNX(}4EOv4%P9BkNpox6%AKRzx#6M@0E z@T-ILyR-bgc<~YomSy6^$snBZyhwYIhP?;#Wy4GG|LWBT96ovlmoNK?_b+az<9u=w z9zJ}AiN6HG+xrqa^pv29c^+Q9dQ*lzKVTVmclW@?9Y+wGnvGq%cEQWb3+5dXv1mmW zoOeplV?YLO-g>NXJU_hi;tt$>-LYuVQj&wYc=YHIE(hF3aOgcTX021SxbiRg^~)Zn zg-Z|>6@|5H*J8n)$PZ%`f5j?!5=i~M3*Fxv@?b{d6o;@QSeivF=TF|QRfP;UYk|&|` zURC-$$;Xet{rmS7%Jup4=h3B07xd{n1{!s@;^fJbO6kg8RzyGU+Ivix_5^(8#x`%} z%$Z=DAm5H0IRa~IYqV?E4jNj=(716cR5Uk_uc*(e_0NT_t!Z!%>6tGO9UTn^2M4ri z(+1`z7r@^_M!#*_wotD%9hMyz!`azco<)WC712L%_!Vr&JceJ;Tl(IqLho(e zx>e|>u&^*8|0bq(u(DnPqsHGs-_R23)%!rt=y#krae~fEu}xCMKt=TB$3#X(V#$&v z*u8rUCD2dD|UgsE3D#Yzl=}MfCjw z(ux25be4lS9$D+j4|O zNiP%A3 zh<B**^`t_a~FA-9`6)G1z+`UYyZsv*Ms@ zl1cti0_ru&B3`o4p?gpnn(_}6(dTs<8yjQw>eWzFZ%AuP!4TWsV%VGop@Obe-(Yxp zC(*kE=r^V7KYm>4>w@JHd2(g$718HT3l=QE%9SgH%&FHLLHiJb5kFnRiZ#I)IW`9E|&zwfxUG#H= z-rYh+t018x-8^!z^0)XhbY%~e(f9HWq`C5G?*m}_gFU`zz6l0R!%&^(FF{{7ljt6% zJ&GZD$Pn-P+em%MBga$FsYfW-Us2SQ(SP{xA!_RR3%$zUh?hDI(qJ_p2aQ_93E5w=G6si_C8M@MGH%?sp@@8i_RnTy0ImP3h@a#qVBa=7 zTO#6sHS1`fbZ_CHOOl97**7|TEDGAjiGsJUyB6zF`8oJr(fT7I(=dDPA7s1U()UiI zxl_fwrN_$t=JWc)kKtIe)(7X#ofE#nwr$(syj?0k-*iu?^!{gNJ;CIuJ8|-_Aet|W zVwcM@az$6B>y@4^{=O2DMKb3p*0XQlK5>nRp!L(fu&mh6Q|uQiTEC3I`}gm$VzsyM zr8w@DZ;9J}Mn+l+hLPXHvcb)&UpU2yF4(&5Abie~EXvENZZ0x1%F2K;`aI{AD_4Y{ zJ!8f!?A*BrN5eDWbS6yrfaAuELuO{C@Ci7ssjiny-#iudj3a2>nb0!{LAx%YsHM-k z`X0P}vUti8ts?r|`QE*I;`>jW=z#4zPKfv-K0Y23CQN|8zrXOq7=O?;Ny56{V}z~X z7=-taTV~b`vWsCz&nS-X#S8kS3Vm5$zV6tu1HG(=3O{Vbh!Nu3T)cQubcu+{N9Ue? z_^MNY{17Zz8IBQS6#GG{(%+f515RE}*t2Jk@JqUO?JD&1lBJtU&?+o^OzXT|;@P1i z*KpjmBt9bRpelW~zl=Ghq@;j-p`k;ELcR7p4ETOM<}UQ7_@gWaC8MG2stWzEzf$4m z7$S6-tE;Ps_qJ@=B5)waq8#SB;unXq-&Pr2**jI}%lh(_F(gMv#}fL8b-Txz^YYzP zzL(LLone&+{)aj;{qk^u|1ZxOCn1PdZ6f{rZ{Q@Vd^(QX)~|QMtXU2s=22ygvObK} z{!cX4|BSy#Boe|0isOU4yj%F9*>tGa=m)lkpW8l{|2J1Qo-}=M7=h_n~EcAKUj9;}Oh1$L`&` zcNe&gOc$~(H#awjsabFGe_SeUqZO^Qs{Hd@EY?fbJjZ^QS15Q;EaU87u-;~jVgCI2 z0te#$jLX!jRSQPOZ76;^gdV*nldqi3ud3WWhkqt2lY7Qd zhQ^~r{4vC4yukhx^;hLtW%e+#ma2%s=C- z{KI=48yhPyaGAd?^byU{bnd1o1{Y^x0ag^xa>Rfh+tZFmnB-#&!Z zz(SIxr{K7hb8#CtZp0sd{2}Dyr=NZjxUsFRt-yO2<76zae*OBwE@*3OLruekWat9g zcN{6OM_*sx%Ce8j@X!0dY1>QKjDC#h*!P5?e573MGn_edM)1$MPxixQ`iJu!t5&T- zP0i6THg$rw{%lmMGZs4aZJ<%Zk}#|W#J@KTjci43Ha$Evz#9E1H9I6db#- zOe|cu5IH$HA|_{D#Id8Bn;U9t4@Y$!KZ+f%Qf@MZt~Z1Xs@Je3{ykAwXPm%5`1~_A zQkhl}|I7p9_~kYBZ`fC0-Oe#5uO%=rP~@TcoObBYLEy8UI(0*vuLhuwc5hVI{1w%; z4-g*TL*RF&%_j-nd;ECia#YNdGX9zODO08p4s9v&5vA9_wujH?;>C;c^5sjx|EW`_ zM9ygadWzrX&PD(J{V{CVFp(eU_>Qq=L&JucGj|2lYLX4mNymWiW{9c1y}c`Kla=v* z_g*2o^f-hXnxjN)CC@*{cZ};B85v=~fB{ALx2zBAP+kM)CAwSn!r;L}FnRK1Oqw(a z+S+=gfAnbYBT&7b7aZnnMRLkbTp>(@{^Obj1#(!bqOW6HdP# zA)oSdu$=NeUC6CeMVv(QGtr_9J=^a29n6 zO9-52?_eGeKqi^VwXvaxLYnc9UH16PO0axhz-Qu3o(= z_MYWkc1Ch@@8bB$K(UXU6YtlrALRkkP(v>sjlUQLs~#g!LpKO@jdCz)>M?BD=0|pf zFrhE5WvfHK$1{Nf66^D7LyS;=j`PaY4ajn0J)c zfz&pXz`EZm*o?Xc&4$@RXS$sZ#BWYXsMA1#QDcL!ZD%lao8%JzIfO-Bf~omMI>XYs zhax7G+$iIpV^uzHg9i`Bnl)=l-+fE`CkWhc`mDc-&cxMN(jhh;Sg_;*t|ew-?jmWv zkoWwTnf}O=6vFyDX)dQ}n^>5(iAVMN`6NRU=$qz=d&c8y)R$no!zCr$mVKp+f0hl_ zA@%Cj6Ztw#O-)*ZHQAr*sHuAzhmV{U6J}@Up?N!&iF+gqiIku4!>+wSXxBNE@w9w9H#WvnZT!y$ajW;YGH#e4dNfQ%gS`DQPcCv#9j%iumVPoNF1{w{H)QKkC-4 zOMVyoEncWrFAR;EpGQl}1abCu?!Jtnqb`zvmrZeH7Rk8OPf&$-?$e}oJaQ$K^Fb@( zpBK-z$k5OT4UC7OhPE^5p_7!i+CXwZbzXIyiC-z7GiLk3KY;XPt6U{AQoP^t>#C`r zLuVy|a$Ci5w9<|L=cOwAbJvZVDUU^UW>T6riT|xa7d15X#Du8<=wNk~@cno?2T`c0 zUn&nKFY}xe)AAI$xSF0s%xC=N6)ap9j=a3P6iYs&dWPI`^D?E^q$>X@sdu2&@NCh( zs%vf`ed&!ySHq-D`yC123@FXlcdZK3i%%b(l%LenQ zLElT71HDaWn)~G!^_{<1nnPlorRTs%n6%Es+4GXpTdnYKRr!DP=rM*2_axdR^E$~G zHvBh%4?3<3!koof7(610Y;HmczKYjyoA#S?hP2uFFQLI zt-tod{H4idYEKm{Kvn+P&aQIwB^$$e$4ofP_YtuY$6D{+y{DRsmx#YsK=vnue1I^q zjv9KQ^j!|qees#)I*9IlBk+rT8p+!g)Ygv{cB^&!SS($62{t4B=={)GwofSXsTA=o zd!{P?+~40n3JscZ{B#`?rg#gVUp9~YmGjBr;qkPmndApQ#)u!?VDJU6=`PiodW-rJ z_Ft2dk|@>*NB<$Gh0c|&Q_DCPhmNGlPg(B0GXA-4*5gb-5vIfbX!pLC#kU_e>a5E6 zE7#bp`hS$j#4+hugaGb`qXzassjq%LcKB!|T&Cxe*B|&t|^>TTZf1!;3moHxv zCbEiZL=KWqC5=m6{|Y8qj)L{T)0M*S@82&ZI~Rt5HYdTc0_QP#9i0E=+9S8qXEA8F zG@q(voJTdcDWC2C^Ez3+4;;!ukA9bN&L@CyrU1&D<`7omEaF7@y(oSElx+81IZVfX z?pZW$8A3U-IPpyxZ^_Ng6+VhYauc%`vfY#FRI|28h>X5dddf0?E8Bnh&WVYsm@@4U zeziX=>La->v2?8<_kZ!|KJueH@tkCB6UCT4-QA0(xqtrwy7oFtbv6OmvSpXZUvQ51 zT4El?{VNt`U!|8g>lU5;E0j@H`Db4=E&T@mJmfCEzdP}D^ytwd-efoVNg+s^XNGg` zuE?c2=AqN3iE}D@KWVB5omB~p&5n{ycN6lz%JHwFeV3hK>GdXDPld>aD!i)7KM&@* z>UHbZfpN0cR6}KNPkH<8JB2K)pqO#~i4c){VJw2{_83oK+>P@9e1_S+*3!Eu_P9=i zT=MV8r`CxjeU?i)G)MS+dQEcZp6h__ihf)drL0wkf1ZcyT=|>t-MbePC;ftf1BZ|- zY{AZeR5*FZh&X}I3}ZsP2LAs7Q&UqgPQ*VNgkN#}+441U*s&*t>ObO1cH*&`>hE1o zar~Z0Ix3QU(J10SADgxWDPv!0{9fKbwq>lpxjs2CFaq6r%*X0An}j{!To+>?`}gk` z{|mx(ie_eJ!d7!_PwQ_2$d3v{Y}^e&o8x}A7xHoF`}ZHPWqTy~gJFb6maXlO=c}^& zFF#gx&pKRMV>}MiNZ#}E^2Gm=aPGy)$w}0M%5oc5Vu;i> zvq;Z+W6?4%8HsYQRpp=g@x9@TbJxy^dIhc@Uc6+jz_YlHfd7|(<%cmiu5*!Z&z`-+ z{KWwnY8Qi;t7U6<7A*D>{;BhJKlxz_@2ko`=i>VM^uc)7aYC-S-jwY#%MaJS8yK2H zQ|CvrPk{>QGB!|HSW<3e9q;59;*3`;oi%CwlI*#nYgPH@xma#_Z#l-~`W?BYv#Rj_`q^6q987|*=Vh_qTuaS+&-gvp zM{^y#MLR1jUUm+r-7k<&dJ*lr`6D^Ca@e>k>!}L=4{kois%~z$ku3e60oJ9AJF_pq z7ABgiab2#3g@wSn*e{cBY`agn z`O;Y}t83)@RptIa!M`eWxxT)#xfNCBmGRI2ry|3ZRT=kxr4RE@nA(5ae|MnDJMeGJ C +