Security
Goal: competitive results come only from legal, real-time gameplay of a registered ruleset. Not a goal, and not claimed: proving a human played, or that a run came from an official build.
Controls
| Threat | Control | Test |
|---|---|---|
| Fabricated score or facts | The server computes score and facts from inputs; client claims are ignored. | integrity.test.ts |
| Modified client simulation, altered seed or rules | State checksum per chunk must match the verifier. | integrity.test.ts |
| Illegal actions | Input bits bounded by the ruleset's button count; rules enforced by the simulation. | integrity.test.ts |
| Speed hacks | Input may lead real time by at most 6 ticks + 0.1%, capped at 60 ticks. | integrity.test.ts |
| Slow motion, late upload, rewritten history | Input must arrive within 1.5 s; sequence and tick continuity; an accepted seq is immutable. | timing.test.ts, integrity.test.ts |
| Pause abuse | Server-approved pauses only, at the flushed tick; 15 min each, 60 min total, 24 h session. Resume removes only the paused time, so pause cycles cannot launder slow motion or bank lead. | timing.test.ts |
| Duplicate rewards | Event id per run; one storage transaction for rewards and outbox; personal bests only increase. | recovery.test.ts, achievements.test.ts |
| Another player's run | Each run lives in its owner's object; the token secret is checked there. | integrity.test.ts |
| Malicious Lua or bytecode | Verifier code is packed by the administrator and deployed. Binary chunks are refused; I/O, loaders and debug are absent; instruction and memory caps. | vm.test.ts |
| Request floods | Body limits before parsing; per-address throttle on every route (IPv6 by /64); guest throttle; run throttle and daily admission spent only after the credential check, inside the player's object; unknown players' objects write no storage. | api.test.ts, integrity.test.ts |
| Verifier faults | Lua errors, Wasm traps and corrupt checkpoints reject only that run (verifier_error); alarms isolate runs from each other. | recovery.test.ts |
| Moderation gaps | Suppressed runs and disabled rulesets are checked again when a result publishes. | recovery.test.ts |
Credentials
- A guest token is
<player_id>.<secret>, 256 random bits of secret. Only its SHA-256 is stored, in the player's object. The token is returned once. It is a bearer credential: anything that reads the game's save can use it. ADMIN_TOKENis a Worker secret of at least 32 characters, compared as hashes. Never ship it or any signing key in a game build.- Native clients require HTTPS and initialized certificate verification. The reference games use the vendored public issuer roots through
scoreosis_tls, which works around Defold 1.13.1's PEM loader defect. Missing roots, a missing extension, or failed initialization stops requests before credentials are sent. Only debug builds may use plain HTTP on loopback for development; the client refuses other insecure URLs before sending credentials.
Known limits
- Bots and assistance that produce legal inputs in real time pass verification. The run seed is returned at start, so a bot can simulate ahead. Moderation (hide, suppress) is the answer; never claim human or cheat-proof verification.
- Players can think during pauses.
- Build ids and platform labels are claims. Platform boards are labels, not attestation.
- Country is Cloudflare's view of the request at run start; a VPN changes it.
- Guest creation cannot stop one person making many guests; throttles slow it.
- Cloudflare's rate-limit binding is per location and eventually consistent. Exact invariants (one active run, sequences) live in the player's object.
- A free plan can be exhausted by abuse. One network address may start at most
MAX_RUNS_PER_ADDRESSranked runs a day, however many guests it creates, so exhausting the daily allowance takes many addresses. An attacker with many IPv4 addresses or IPv6 /64s can still do it. The counter stores an HMAC of the address underADDRESS_KEY, kept one day; anyone holding both the key and the table can still recover IPv4 addresses by brute force. Capacity errors stop new ranked runs; local play continues. - itch.io serves every HTML5 game from the shared
html-classic.itch.zoneorigin, so other games there can read browser storage, including the guest token. Treat web guest tokens as low-value; anything more needs per-game hosting.
Logging
Structured events only (src/log.ts). Never log tokens, authorization headers or IP addresses.
Revoking achievements
Achievement state lives in each player's object (bt_achievements, bt_scores, bt_events). To revoke after a compromised ruleset: disable the ruleset (POST /v1/admin/games/:game/rulesets/:ruleset/disable), then for each affected player run a one-off script with wrangler against the object that deletes the run's bt_events row and recomputes bt_scores and bt_achievements by replaying the remaining events through a fresh engine. No endpoint does this automatically; it rewrites player progress and needs review per incident.