typefaster-cli 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. typefaster_cli-0.1.2/.github/dependabot.yml +16 -0
  2. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/workflows/ci.yml +15 -0
  3. typefaster_cli-0.1.2/.github/workflows/codeql.yml +23 -0
  4. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/workflows/release.yml +27 -0
  5. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/PKG-INFO +1 -1
  6. typefaster_cli-0.1.2/SECURITY.md +28 -0
  7. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/__init__.py +1 -1
  8. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/token_store.py +4 -1
  9. typefaster_cli-0.1.2/docs/deploy-fly.md +61 -0
  10. typefaster_cli-0.1.2/fly.toml +38 -0
  11. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/pyproject.toml +1 -1
  12. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/deps.py +16 -0
  13. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/repositories.py +8 -0
  14. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/auth.py +12 -4
  15. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/oauth.py +6 -6
  16. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_oauth.py +1 -2
  17. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.dockerignore +0 -0
  18. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.env.example +0 -0
  19. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  21. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.gitignore +0 -0
  24. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/CONTRIBUTING.md +0 -0
  25. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/LICENSE +0 -0
  26. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/Makefile +0 -0
  27. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/README.md +0 -0
  28. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/Dockerfile +0 -0
  29. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/__main__.py +0 -0
  30. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/assets/__init__.py +0 -0
  31. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/assets/quotes.json +0 -0
  32. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/cli.py +0 -0
  33. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/__init__.py +0 -0
  34. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/anti_cheat.py +0 -0
  35. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/calculators.py +0 -0
  36. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/errors.py +0 -0
  37. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/ghost.py +0 -0
  38. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/models.py +0 -0
  39. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/typing_engine.py +0 -0
  40. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/__init__.py +0 -0
  41. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/clock.py +0 -0
  42. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/config.py +0 -0
  43. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/db.py +0 -0
  44. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/migrations.py +0 -0
  45. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/paths.py +0 -0
  46. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/quote_loader.py +0 -0
  47. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/replay_store.py +0 -0
  48. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/repository.py +0 -0
  49. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/sqlite_repository.py +0 -0
  50. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/__init__.py +0 -0
  51. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/api.py +0 -0
  52. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/commands.py +0 -0
  53. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/__init__.py +0 -0
  54. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/container.py +0 -0
  55. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/daily_service.py +0 -0
  56. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/ghost_service.py +0 -0
  57. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/profile_service.py +0 -0
  58. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/race_service.py +0 -0
  59. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/stats_service.py +0 -0
  60. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/__init__.py +0 -0
  61. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/app.py +0 -0
  62. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/online_app.py +0 -0
  63. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/__init__.py +0 -0
  64. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/_base.py +0 -0
  65. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/daily.py +0 -0
  66. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/help.py +0 -0
  67. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/history.py +0 -0
  68. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/leaderboard.py +0 -0
  69. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/main_menu.py +0 -0
  70. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/online_race.py +0 -0
  71. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/practice.py +0 -0
  72. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/profile.py +0 -0
  73. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/race.py +0 -0
  74. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/results.py +0 -0
  75. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/settings.py +0 -0
  76. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/stats.py +0 -0
  77. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/theme.py +0 -0
  78. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/__init__.py +0 -0
  79. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/bigtext.py +0 -0
  80. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/live_stats.py +0 -0
  81. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/progress_bars.py +0 -0
  82. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/typing_field.py +0 -0
  83. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docker-compose.yml +0 -0
  84. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/RELEASING.md +0 -0
  85. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/api-spec.md +0 -0
  86. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/architecture.md +0 -0
  87. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/deployment.md +0 -0
  88. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/online-setup.md +0 -0
  89. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/redis-schema.md +0 -0
  90. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/roadmap.md +0 -0
  91. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/sqlite-schema.md +0 -0
  92. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/ui-design.md +0 -0
  93. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/websocket-protocol.md +0 -0
  94. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/infra/README.md +0 -0
  95. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/infra/nginx/nginx.conf +0 -0
  96. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/infra/redis/redis.conf +0 -0
  97. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/packaging/homebrew/typefaster.rb +0 -0
  98. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/scripts/seed_quotes.py +0 -0
  99. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/Dockerfile +0 -0
  100. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/__init__.py +0 -0
  101. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/config.py +0 -0
  102. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/logging_config.py +0 -0
  103. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/main.py +0 -0
  104. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/quotes.py +0 -0
  105. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/redis_keys.py +0 -0
  106. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/__init__.py +0 -0
  107. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/health.py +0 -0
  108. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/leaderboards.py +0 -0
  109. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/lobbies.py +0 -0
  110. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/security.py +0 -0
  111. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/ws/__init__.py +0 -0
  112. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/ws/manager.py +0 -0
  113. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/pyproject.toml +0 -0
  114. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/__init__.py +0 -0
  115. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/conftest.py +0 -0
  116. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_auth.py +0 -0
  117. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_health_leaderboards.py +0 -0
  118. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_lobbies.py +0 -0
  119. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_scoring_anticheat.py +0 -0
  120. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_ws_race.py +0 -0
  121. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/pyproject.toml +0 -0
  122. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/__init__.py +0 -0
  123. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/anti_cheat.py +0 -0
  124. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/dto.py +0 -0
  125. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/events.py +0 -0
  126. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/scoring.py +0 -0
  127. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/conftest.py +0 -0
  128. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/fixtures/.gitkeep +0 -0
  129. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_container_and_cli.py +0 -0
  130. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_migrations.py +0 -0
  131. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_profile_stats.py +0 -0
  132. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_race_service.py +0 -0
  133. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_race_timing.py +0 -0
  134. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_sqlite_repository.py +0 -0
  135. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_ui_smoke.py +0 -0
  136. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_anti_cheat.py +0 -0
  137. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_calculators.py +0 -0
  138. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_config.py +0 -0
  139. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_ghost.py +0 -0
  140. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_net.py +0 -0
  141. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_quote_loader.py +0 -0
  142. {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_typing_engine.py +0 -0
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: "/" # client (root pyproject)
5
+ schedule: { interval: weekly }
6
+ open-pull-requests-limit: 5
7
+ - package-ecosystem: pip
8
+ directory: "/server"
9
+ schedule: { interval: weekly }
10
+ open-pull-requests-limit: 5
11
+ - package-ecosystem: pip
12
+ directory: "/shared"
13
+ schedule: { interval: weekly }
14
+ - package-ecosystem: github-actions
15
+ directory: "/"
16
+ schedule: { interval: weekly }
@@ -66,6 +66,21 @@ jobs:
66
66
  - name: Test (pytest)
67
67
  run: cd server && pytest
68
68
 
69
+ audit:
70
+ name: dependency audit (pip-audit)
71
+ runs-on: ubuntu-latest
72
+ steps:
73
+ - uses: actions/checkout@v4
74
+ - uses: actions/setup-python@v5
75
+ with:
76
+ python-version: "3.12"
77
+ cache: pip
78
+ - run: python -m pip install pip-audit
79
+ - name: Audit client deps
80
+ run: pip-audit --strict --progress-spinner off . || true
81
+ - name: Audit server deps
82
+ run: pip-audit --strict --progress-spinner off ./server || true
83
+
69
84
  build:
70
85
  name: build wheel
71
86
  runs-on: ubuntu-latest
@@ -0,0 +1,23 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: "0 6 * * 1" # weekly
10
+
11
+ jobs:
12
+ analyze:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ security-events: write
16
+ actions: read
17
+ contents: read
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: github/codeql-action/init@v3
21
+ with:
22
+ languages: python
23
+ - uses: github/codeql-action/analyze@v3
@@ -13,6 +13,7 @@ on:
13
13
  permissions:
14
14
  contents: write # create the GitHub Release
15
15
  id-token: write # OIDC token for PyPI Trusted Publishing
16
+ packages: write # push the server image to GHCR
16
17
 
17
18
  jobs:
18
19
  build:
@@ -53,3 +54,29 @@ jobs:
53
54
  with:
54
55
  generate_release_notes: true
55
56
  files: dist/*
57
+
58
+ server-image:
59
+ name: build & push server image (GHCR)
60
+ needs: build
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - uses: docker/login-action@v3
65
+ with:
66
+ registry: ghcr.io
67
+ username: ${{ github.actor }}
68
+ password: ${{ secrets.GITHUB_TOKEN }}
69
+ - id: meta
70
+ uses: docker/metadata-action@v5
71
+ with:
72
+ images: ghcr.io/${{ github.repository_owner }}/typefaster-server
73
+ tags: |
74
+ type=semver,pattern={{version}}
75
+ type=raw,value=latest
76
+ - uses: docker/build-push-action@v6
77
+ with:
78
+ context: .
79
+ file: server/Dockerfile
80
+ push: true
81
+ tags: ${{ steps.meta.outputs.tags }}
82
+ labels: ${{ steps.meta.outputs.labels }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typefaster-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A terminal-first typing game inspired by MonkeyType and TypeRacer.
5
5
  Project-URL: Homepage, https://github.com/Anoshor/typefaster-cli
6
6
  Project-URL: Repository, https://github.com/Anoshor/typefaster-cli
@@ -0,0 +1,28 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+ The latest released version on PyPI (`typefaster-cli`) and the `main` branch
5
+ receive security fixes.
6
+
7
+ ## Reporting a vulnerability
8
+ Please **do not open a public issue** for security problems.
9
+
10
+ Report privately via GitHub's **Security advisories**:
11
+ <https://github.com/Anoshor/typefaster-cli/security/advisories/new>
12
+
13
+ (or email the maintainer listed on the GitHub profile). Include steps to
14
+ reproduce and impact. We aim to acknowledge within 72 hours and to ship a fix or
15
+ mitigation promptly, crediting reporters who wish to be named.
16
+
17
+ ## Hardening notes (for self-hosters)
18
+ - Run the server only behind TLS (the bundled nginx profile, or a TLS-terminating
19
+ platform like Fly.io). Never expose Redis publicly — keep it on the internal
20
+ network (`docker-compose.yml` does not publish 6379).
21
+ - Set a strong `TYPEFASTER_JWT_SECRET` (e.g. `openssl rand -hex 32`); rotating it
22
+ invalidates all existing sessions.
23
+ - Lock `TYPEFASTER_CORS_ORIGINS` to your real domain in production (default `*`).
24
+ - Credential endpoints (`/auth/register`, `/auth/login`, OAuth start) are
25
+ per-IP rate limited; keep `--proxy-headers` enabled so the limiter sees real
26
+ client IPs behind a proxy.
27
+ - Passwords are hashed with bcrypt; results are re-scored server-side with
28
+ anti-cheat before leaderboard writes.
@@ -1,3 +1,3 @@
1
1
  """TYPEFASTER-CLI — a terminal-first typing game."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.2"
@@ -15,7 +15,10 @@ def _auth_path() -> Path:
15
15
 
16
16
  @dataclass(slots=True)
17
17
  class Session:
18
- server_url: str = "http://localhost:8000"
18
+ # Default points at the public TYPEFASTER server so a fresh install can
19
+ # register/play with zero config. Override with `typefaster config set-server`
20
+ # (e.g. a local server or your own deployment).
21
+ server_url: str = "https://typefaster-cli.fly.dev"
19
22
  token: str | None = None
20
23
  username: str | None = None
21
24
 
@@ -0,0 +1,61 @@
1
+ # Deploy the server to Fly.io (free-tier friendly)
2
+
3
+ Gives you a permanent `https://<app>.fly.dev` so anyone can play with **zero
4
+ config** (the client already defaults to `https://typefaster-cli.fly.dev`).
5
+
6
+ ## Prereqs
7
+ ```bash
8
+ brew install flyctl
9
+ fly auth signup # or: fly auth login
10
+ ```
11
+
12
+ ## 1. Create the app (uses the repo's fly.toml)
13
+ ```bash
14
+ cd typefaster-cli
15
+ fly launch --no-deploy --copy-config --name typefaster-cli --region bom
16
+ ```
17
+ If `typefaster-cli` is taken, pick another name — then update `app` in
18
+ `fly.toml` **and** the default in `client/typefaster/net/token_store.py`, and
19
+ re-release the client so the baked-in default matches.
20
+
21
+ ## 2. Redis (free Upstash)
22
+ ```bash
23
+ fly redis create # choose the free plan; copy the rediss://… URL it prints
24
+ ```
25
+
26
+ ## 3. Secrets
27
+ ```bash
28
+ fly secrets set \
29
+ TYPEFASTER_JWT_SECRET="$(openssl rand -hex 32)" \
30
+ TYPEFASTER_REDIS_URL="rediss://…from step 2…" \
31
+ TYPEFASTER_CORS_ORIGINS="*"
32
+ # Optional social login (see online-setup.md):
33
+ # fly secrets set TYPEFASTER_GITHUB_CLIENT_ID=... TYPEFASTER_GOOGLE_CLIENT_ID=... TYPEFASTER_GOOGLE_CLIENT_SECRET=...
34
+ ```
35
+
36
+ ## 4. Deploy
37
+ ```bash
38
+ fly deploy
39
+ fly status
40
+ curl -s https://typefaster-cli.fly.dev/healthz # {"status":"ok"}
41
+ curl -s https://typefaster-cli.fly.dev/readyz # {"status":"ready","redis":"ok"}
42
+ ```
43
+
44
+ ## 5. Play (anyone, anywhere)
45
+ ```bash
46
+ pipx install typefaster-cli
47
+ typefaster register alice # uses the Fly server by default — no config needed
48
+ typefaster lobby create --name Friday --time 60
49
+ ```
50
+ WebSockets work automatically over Fly's TLS (`wss://`).
51
+
52
+ ## Updating the server later
53
+ - Manually: `fly deploy` from the repo.
54
+ - Automated (optional): the release workflow already builds & pushes the server
55
+ image to `ghcr.io/anoshor/typefaster-server`; add a `fly deploy --image …`
56
+ step (or `flyctl deploy` GitHub Action with `FLY_API_TOKEN`) to auto-deploy on
57
+ each tag.
58
+
59
+ ## Costs
60
+ Fly's free allowance covers one small always-on machine + free Upstash Redis —
61
+ $0 for a hobby server. Heavier usage may incur small charges; check Fly billing.
@@ -0,0 +1,38 @@
1
+ # Fly.io deployment for the TYPEFASTER server.
2
+ # fly launch --no-deploy # first time (creates the app; keep this file)
3
+ # fly redis create # free Upstash Redis -> copy the rediss:// URL
4
+ # fly secrets set TYPEFASTER_JWT_SECRET=$(openssl rand -hex 32) \
5
+ # TYPEFASTER_REDIS_URL='rediss://…' \
6
+ # TYPEFASTER_CORS_ORIGINS='*'
7
+ # fly deploy
8
+ #
9
+ # If the app name below is taken, change it (and the client default in
10
+ # client/typefaster/net/token_store.py) to match your real app URL.
11
+
12
+ app = "typefaster-cli"
13
+ primary_region = "bom" # Mumbai; change to your nearest region
14
+
15
+ [build]
16
+ dockerfile = "server/Dockerfile"
17
+
18
+ [env]
19
+ TYPEFASTER_CORS_ORIGINS = "*"
20
+
21
+ [http_service]
22
+ internal_port = 8000
23
+ force_https = true
24
+ # Keep one machine warm so WebSocket races aren't dropped by cold starts.
25
+ auto_stop_machines = false
26
+ auto_start_machines = true
27
+ min_machines_running = 1
28
+
29
+ [[http_service.checks]]
30
+ interval = "30s"
31
+ timeout = "5s"
32
+ grace_period = "10s"
33
+ method = "get"
34
+ path = "/healthz"
35
+
36
+ [[vm]]
37
+ size = "shared-cpu-1x"
38
+ memory = "512mb"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "typefaster-cli"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "A terminal-first typing game inspired by MonkeyType and TypeRacer."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -23,6 +23,22 @@ SettingsDep = Annotated[Settings, Depends(settings_dep)]
23
23
  RepoDep = Annotated[RedisRepository, Depends(repo_dep)]
24
24
 
25
25
 
26
+ def rate_limiter(bucket: str, limit: int, window_seconds: int): # type: ignore[no-untyped-def]
27
+ """Build a per-IP fixed-window rate-limit dependency for a route."""
28
+
29
+ async def _dep(request: Request) -> None:
30
+ ip = request.client.host if request.client else "unknown"
31
+ repo = repo_dep(request)
32
+ count = await repo.incr_rate(f"rl:{bucket}:{ip}", window_seconds)
33
+ if count > limit:
34
+ raise HTTPException(
35
+ status.HTTP_429_TOO_MANY_REQUESTS,
36
+ "Too many requests — please slow down.",
37
+ )
38
+
39
+ return _dep
40
+
41
+
26
42
  async def current_user(
27
43
  repo: RepoDep,
28
44
  settings: SettingsDep,
@@ -110,6 +110,14 @@ class RedisRepository:
110
110
  if wpm > current:
111
111
  await self.r.hset(key, "best_wpm", wpm)
112
112
 
113
+ # ── rate limiting ──────────────────────────────────────────────────
114
+ async def incr_rate(self, key: str, window_seconds: int) -> int:
115
+ """Increment a fixed-window counter, returning the new count."""
116
+ n = int(await self.r.incr(key))
117
+ if n == 1:
118
+ await self.r.expire(key, window_seconds)
119
+ return n
120
+
113
121
  # ── sessions ───────────────────────────────────────────────────────
114
122
  async def create_session(self, jti: str, username: str, ttl_seconds: int) -> None:
115
123
  await self.r.set(keys.session(jti), username, ex=ttl_seconds)
@@ -5,16 +5,24 @@ from __future__ import annotations
5
5
  from typing import Annotated
6
6
 
7
7
  import jwt
8
- from fastapi import APIRouter, Header, HTTPException, status
8
+ from fastapi import APIRouter, Depends, Header, HTTPException, status
9
9
  from typefaster_shared.dto import LoginRequest, RegisterRequest, TokenResponse, UserPublic
10
10
 
11
- from ..deps import CurrentUser, RepoDep, SettingsDep
11
+ from ..deps import CurrentUser, RepoDep, SettingsDep, rate_limiter
12
12
  from ..security import create_access_token, hash_password, verify_password
13
13
 
14
14
  router = APIRouter(prefix="/auth", tags=["auth"])
15
15
 
16
+ # Per-IP throttle on credential endpoints (abuse / brute-force protection).
17
+ _auth_limit = Depends(rate_limiter("auth", limit=15, window_seconds=60))
16
18
 
17
- @router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
19
+
20
+ @router.post(
21
+ "/register",
22
+ response_model=TokenResponse,
23
+ status_code=status.HTTP_201_CREATED,
24
+ dependencies=[_auth_limit],
25
+ )
18
26
  async def register(body: RegisterRequest, repo: RepoDep, settings: SettingsDep) -> TokenResponse:
19
27
  created = await repo.create_user(body.username, hash_password(body.password))
20
28
  if not created:
@@ -24,7 +32,7 @@ async def register(body: RegisterRequest, repo: RepoDep, settings: SettingsDep)
24
32
  return TokenResponse(access_token=token, username=body.username)
25
33
 
26
34
 
27
- @router.post("/login", response_model=TokenResponse)
35
+ @router.post("/login", response_model=TokenResponse, dependencies=[_auth_limit])
28
36
  async def login(body: LoginRequest, repo: RepoDep, settings: SettingsDep) -> TokenResponse:
29
37
  user = await repo.get_user(body.username)
30
38
  if not user or not verify_password(body.password, user["password_hash"]):
@@ -6,9 +6,9 @@ TYPEFASTER JWT. No client secrets live on the user's machine; Google's secret
6
6
  stays server-side. Both providers are free.
7
7
 
8
8
  Endpoints:
9
- POST /auth/oauth/{provider}/start -> { device_code, user_code, verification_uri, interval, expires_in }
10
- POST /auth/oauth/{provider}/poll -> { status: "pending"|"slow_down" } (200) or { access_token, username } (200)
11
- terminal errors -> 400
9
+ POST /auth/oauth/{provider}/start -> device_code, user_code, verification_uri, interval
10
+ POST /auth/oauth/{provider}/poll -> {status: pending|slow_down} (200), or a
11
+ TokenResponse (200) when authorized; terminal errors -> 400.
12
12
  """
13
13
 
14
14
  from __future__ import annotations
@@ -16,12 +16,12 @@ from __future__ import annotations
16
16
  from typing import Any
17
17
 
18
18
  import httpx
19
- from fastapi import APIRouter, HTTPException, status
19
+ from fastapi import APIRouter, Depends, HTTPException, status
20
20
  from pydantic import BaseModel
21
21
  from typefaster_shared.dto import TokenResponse
22
22
 
23
23
  from ..config import Settings
24
- from ..deps import RepoDep, SettingsDep
24
+ from ..deps import RepoDep, SettingsDep, rate_limiter
25
25
  from ..repositories import RedisRepository
26
26
  from ..security import create_access_token
27
27
 
@@ -63,7 +63,7 @@ def _check_provider(provider: str) -> None:
63
63
  raise HTTPException(status.HTTP_404_NOT_FOUND, "Unknown provider")
64
64
 
65
65
 
66
- @router.post("/{provider}/start")
66
+ @router.post("/{provider}/start", dependencies=[Depends(rate_limiter("oauth", 20, 60))])
67
67
  async def start(provider: str, settings: SettingsDep) -> dict[str, Any]:
68
68
  _check_provider(provider)
69
69
  cid = _client_id(provider, settings)
@@ -4,9 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import fakeredis.aioredis
6
6
  import pytest
7
- from starlette.testclient import TestClient
8
-
9
7
  from app.repositories import RedisRepository
8
+ from starlette.testclient import TestClient
10
9
 
11
10
 
12
11
  def test_unknown_provider_404(client: TestClient) -> None:
File without changes
File without changes
File without changes