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.
- typefaster_cli-0.1.2/.github/dependabot.yml +16 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/workflows/ci.yml +15 -0
- typefaster_cli-0.1.2/.github/workflows/codeql.yml +23 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/workflows/release.yml +27 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/PKG-INFO +1 -1
- typefaster_cli-0.1.2/SECURITY.md +28 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/__init__.py +1 -1
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/token_store.py +4 -1
- typefaster_cli-0.1.2/docs/deploy-fly.md +61 -0
- typefaster_cli-0.1.2/fly.toml +38 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/pyproject.toml +1 -1
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/deps.py +16 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/repositories.py +8 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/auth.py +12 -4
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/oauth.py +6 -6
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_oauth.py +1 -2
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.dockerignore +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.env.example +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/.gitignore +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/CONTRIBUTING.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/LICENSE +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/Makefile +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/README.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/Dockerfile +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/__main__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/assets/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/assets/quotes.json +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/cli.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/anti_cheat.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/calculators.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/errors.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/ghost.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/models.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/domain/typing_engine.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/clock.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/config.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/db.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/migrations.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/paths.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/quote_loader.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/replay_store.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/repository.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/infra/sqlite_repository.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/api.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/net/commands.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/container.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/daily_service.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/ghost_service.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/profile_service.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/race_service.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/services/stats_service.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/app.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/online_app.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/_base.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/daily.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/help.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/history.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/leaderboard.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/main_menu.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/online_race.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/practice.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/profile.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/race.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/results.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/settings.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/screens/stats.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/theme.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/bigtext.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/live_stats.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/progress_bars.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/client/typefaster/ui/widgets/typing_field.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docker-compose.yml +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/RELEASING.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/api-spec.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/architecture.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/deployment.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/online-setup.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/redis-schema.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/roadmap.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/sqlite-schema.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/ui-design.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/docs/websocket-protocol.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/infra/README.md +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/infra/nginx/nginx.conf +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/infra/redis/redis.conf +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/packaging/homebrew/typefaster.rb +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/scripts/seed_quotes.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/Dockerfile +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/config.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/logging_config.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/main.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/quotes.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/redis_keys.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/health.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/leaderboards.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/routers/lobbies.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/security.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/ws/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/app/ws/manager.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/pyproject.toml +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/conftest.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_auth.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_health_leaderboards.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_lobbies.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_scoring_anticheat.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/server/tests/test_ws_race.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/pyproject.toml +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/__init__.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/anti_cheat.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/dto.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/events.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/shared/typefaster_shared/scoring.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/conftest.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/fixtures/.gitkeep +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_container_and_cli.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_migrations.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_profile_stats.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_race_service.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_race_timing.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_sqlite_repository.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/integration/test_ui_smoke.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_anti_cheat.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_calculators.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_config.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_ghost.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_net.py +0 -0
- {typefaster_cli-0.1.1 → typefaster_cli-0.1.2}/tests/unit/test_quote_loader.py +0 -0
- {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.
|
|
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.
|
|
@@ -15,7 +15,10 @@ def _auth_path() -> Path:
|
|
|
15
15
|
|
|
16
16
|
@dataclass(slots=True)
|
|
17
17
|
class Session:
|
|
18
|
-
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
|
10
|
-
POST /auth/oauth/{provider}/poll
|
|
11
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|