srdusr
aboutsummaryrefslogtreecommitdiffstats

TyperPunk

Competitive typing for the terminal and the browser. It has solo practice, live multiplayer races, code and command drills, and a practice mode that targets the keys you personally get wrong.

The web client and the terminal client share one Rust core, so both score a test the same way.

Contents

What it does

  • Solo tests by word count, by time, or on a fixed passage.
  • Live races against other people, or against bots when nobody else is around.
  • Code and command drills. Each one explains what the line does.
  • Practice mode, which generates text weighted toward the characters you mistype or hesitate on.
  • Custom text. Import your own notes and work through them over several sittings.
  • Accounts, personal bests, a leaderboard, and friends.

Running it

You need Rust and Node.js. The web client also needs wasm-pack, which web/launch.sh installs if it is missing.

Web client

./web/launch.sh

This builds the WebAssembly core, merges the text packs, and serves the app on http://localhost:4173. There is no bundler and no npm dependency tree. Edit a file under web/src/ and reload the page.

The multiplayer, account and leaderboard features need the server as well. See Server.

Terminal client

cargo run --package typerpunk-tui

./install.sh builds it and puts it on your path.

Modes

Mode What it types
Random A passage from any pack
Words A fixed number of common words
Timed As many words as you can before the clock runs out
Zen No timer and no word limit
Practice Words weighted toward your own weak keys
Custom Text you import, including markdown notes
Lyrics The song currently playing on Spotify

Alongside those are the text packs, listed below.

Sixteen typing languages are available for the generated-word modes. This sets the vocabulary you type, not the language of the interface. The interface is English only. See TODO-ui-languages.md.

Text packs

data/packs/*.json holds the passages. Each entry looks like this:

{
  "category": "quotes",
  "content": "The best way out is always through.",
  "attribution": "Robert Frost"
}

attribution is optional. Leave it out for original prose rather than inventing a source. Code entries take two more fields:

{
  "category": "shell",
  "language": "shell",
  "content": "awk -F: '{print $1, $7}' /etc/passwd",
  "attribution": "awk",
  "explanation": "-F sets the field separator."
}

language is one of javascript, python, rust, clike or shell, and selects the syntax highlighting. explanation is shown while you type in single player, and after the race in multiplayer.

After editing a pack, rebuild the merged dataset:

node scripts/merge_packs.js

This writes texts.json and web/src/data/texts.json.

Passages must be a single line. The typing input is one line, so a passage containing a newline cannot be finished.

Community submissions

Signed-in users can submit passages from the Contribute screen. Nothing reaches other players until a moderator approves it. Approved passages are served from the database and merged over the bundled packs when the client starts.

To fold approved submissions back into the repository:

node scripts/export_approved.js http://localhost:8787
node scripts/merge_packs.js

The first administrator is named by TYPERPUNK_ADMIN_USERNAME at startup. Administrators appoint moderators from the Contribute screen.

Server

typerpunk-server provides accounts, stats, the leaderboard, friends, multiplayer rooms and the Spotify integration. It needs PostgreSQL.

sudo -u postgres createuser --pwprompt typerpunk
sudo -u postgres createdb -O typerpunk typerpunk
sudo -u postgres createdb -O typerpunk typerpunk_test

cp crates/server/.env.example crates/server/.env
cargo run --package typerpunk-server

Migrations run at startup. Configuration is by environment variable; see crates/server/.env.example for the full list.

Variable Purpose
DATABASE_URL PostgreSQL connection string
PORT Listen port, default 8787
FRONTEND_ORIGIN Origin allowed by CORS
COOKIE_SECURE Set to 1 behind TLS
TYPERPUNK_ENV Set to production to enforce the checks below
TYPERPUNK_ADMIN_USERNAME Account to make an administrator at startup
TEXTS_JSON_PATH Dataset the race passages come from
SPOTIFY_CLIENT_ID Spotify application ID, for Lyrics mode
SPOTIFY_CLIENT_SECRET Spotify application secret
STRIPE_SECRET_KEY Stripe API key, for the store
STRIPE_WEBHOOK_SECRET Signing secret for the Stripe webhook

Store

The store sells cosmetics and a supporter subscription. Checkout is hosted by Stripe: the buyer is redirected there, pays there, and returns. No card details reach this server.

Nothing is granted at checkout. Stripe calls /api/billing/webhook when the payment succeeds, the server verifies the signature, and only then does the item appear. Set both Stripe variables. With either one missing, the store still displays but every checkout answers 501.

Prices come from the cosmetics, bundles and merch tables. The client sends an item id, never an amount.

Shirts, mugs and deskmats are sold from the same store. These need an address, which Stripe collects on its own checkout page, so no postal detail is entered on this site. Postage is charged as a separate line so the buyer can see it.

A physical order unlocks nothing. It records an obligation to pack and post something. /api/admin/orders lists what has been paid for and not yet sent, with the address, and the Contribute screen shows the same list to an administrator. Mark an order posted there once it goes out. Nothing emails the buyer; that is still a manual step.

Shipping is limited to the countries in SHIPPING_COUNTRIES in crates/server/src/billing.rs. Add to that list only where you are willing to post and handle returns.

Deployment

Put a reverse proxy in front. Serve web/ as static files and route /api/* and /ws/* to the server. That makes the API same-origin, so no CORS configuration is needed in the browser.

Set TYPERPUNK_ENV=production. The server then refuses to start if:

  • COOKIE_SECURE is not 1, which would send the session cookie in clear.
  • DATABASE_URL is still the development default.
  • FRONTEND_ORIGIN is http:// on a host that is not local.

A warning in a log nobody reads is not a safeguard, so these are refusals rather than warnings.

Terminate TLS at the proxy and send HSTS from there. The application sets the other security headers itself.

Secrets

Every setting is read from the environment. Copy crates/server/.env.example to crates/server/.env for local work. That file is gitignored and is the only place a password or a client secret belongs.

In production, prefer real environment variables to a file on disk. A systemd unit can take them from EnvironmentFile=, with the file owned by root and mode 600:

[Service]
EnvironmentFile=/etc/typerpunk/env
ExecStart=/usr/local/bin/typerpunk-server
User=typerpunk

Container runtimes and hosting platforms have their own secret stores. The server reads plain environment variables in every case, so nothing in the application changes.

Do not put a secret in .env.example, in a commit message, or in an issue. If one is exposed, rotate it: change the database password, restart the server, and invalidate sessions by clearing the sessions table.

The first administrator is created by setting TYPERPUNK_ADMIN_USERNAME to an account that has already registered. Unset it afterwards. It exists to create the first administrator and to recover if the last one is removed.

Development

cargo test --workspace --exclude typerpunk-steam    # Rust
cd web/tests && python3 run_all.py                  # browser

The browser tests drive the real application with Playwright. They need both servers running; see web/tests/README.md.

crates/steam is a Bevy desktop client and is excluded from the default test run because it is slow to build.

Repository layout:

crates/core      typing engine, shared by every client
crates/wasm      WebAssembly bindings for the web client
crates/tui       terminal client
crates/server    HTTP and WebSocket server
crates/steam     desktop client (Bevy)
web/             web client, no build step
data/packs/      text packs
scripts/         dataset tools

Terminal client parity

The terminal client has accounts, the leaderboard and friends. Multiplayer racing is on hold there: it needs a WebSocket client and a live-updating race view, which is close to the size of the whole web multiplayer build.

Security

  • Passwords are hashed with Argon2. Sessions are HttpOnly, SameSite=Lax cookies, and Secure when COOKIE_SECURE is set.
  • Every query is parameterised. Every value rendered into the page is escaped, including imported file content and file names.
  • Custom text is read in the browser and never uploaded.
  • The Spotify and lyrics endpoints have fixed upstream hosts, so neither can be pointed elsewhere. Both are rate limited.
  • The client sends a Content-Security-Policy that forbids inline script.
  • Dependencies are checked against the OSV database. The server build has no known advisories.

Report a security problem by opening an issue, or privately if it is exploitable.

Licence

MIT. See LICENSE.