openvibers/openvibe.play README.md
README.md
| 1 | # OpenVibe.Play |
| 2 | |
| 3 | **Achievements, leaderboards and cloud saves for every OpenVibe game.** https://play.openvibe.network |
| 4 | |
| 5 | **Live site:** https://play.openvibe.network · **API docs:** https://openvibe.services · **For AI:** https://play.openvibe.network/llms.txt |
| 6 | **Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test` |
| 7 | |
| 8 | OpenVibe.Play is the network's game services (OpenVibe.Games [ADR-0007](https://github.com/OpenVibers/OpenVibe.Games/blob/main/docs/adr/0007-rewrite-the-moddable-base.md) |
| 9 | decision 10). A game registers its **title** with its own OpenVibe token, posts what its players did, and anyone reads |
| 10 | its boards. The rules of each game stay in the game; Play keeps the record and answers it. |
| 11 | |
| 12 | - **Titles** belong to the principal that registered them (a game's service or app token). Only that principal writes to |
| 13 | a title; to anyone else it does not exist. |
| 14 | - **Your own game, from the sandbox** (ADR-014 opt-in): any developer makes a project on openvibe.services and a sandbox |
| 15 | app with Play's three capabilities (in Network's default sandbox allowance). A sandbox app registers up to 3 titles |
| 16 | whose ids start with `sandbox-`, with guest players only (nothing adopted, no person's record touched), never listed, |
| 17 | counted or indexed: readable at their id, marked `sandbox: true`, and deleted by their app (`DELETE /titles/:title`; |
| 18 | a production title is never deleted). A production app (staff turn production on for the |
| 19 | project) registers the real name; it never takes a `sandbox-` id. |
| 20 | - **Achievements**, including hidden ones whose name shows only once unlocked. A definition left out of an update is |
| 21 | retired, never deleted: what players earned stays. |
| 22 | - **Leaderboards** of three kinds. `best` ranks each player's best score. `sum` ranks the total of theirs. `time` |
| 23 | ranks each player's fastest run, in whole milliseconds (at most 24 hours), fastest first. Each reads this week (from |
| 24 | Monday 00:00 UTC) or all time, 25 a page, and a tie goes to whoever got there first. A board's `unit` says what its |
| 25 | scores mean: `"ms"` for `time`, `"points"` for the others. |
| 26 | - **Players** are people (`usr_…`) or guests a game names (`guest:<opaque>`). When a guest signs in, the game adopts |
| 27 | them and their unlocks and scores become the person's. |
| 28 | - **Cloud saves** (`migrations/0004_saves.sql`): a player's progress follows them to any device. Up to 8 slots per |
| 29 | player per title, each any JSON value of at most 64 KB, versioned: a write names the version it read and a stale one |
| 30 | is refused (409 `play.save_conflict`). The title's own token writes (`play.result.write`) and reads |
| 31 | (`play.player.read`); a person lists and deletes their own at `/me/saves` and sees them on /me. A guest's slots move |
| 32 | to the person on adoption (the newer save wins a clash); they are in the account export and erased with the account. |
| 33 | - **Idempotent writes.** A round's scores carry a `ref`; posting it again changes nothing. An unlock answers only the |
| 34 | achievements that were new. |
| 35 | - **Personal data.** Only people's rows are: they are in the person's account export and erased with their account |
| 36 | (ADR-033). A board shows a name and a score, never a player id. |
| 37 | |
| 38 | Every page is server-rendered and readable without JavaScript: the home (every game and this week's leaders), a title, |
| 39 | a board, your achievements and the API docs. |
| 40 | |
| 41 | ## Purpose |
| 42 | |
| 43 | One record of what players did in OpenVibe games, shared by every game and read anywhere. A game keeps its own rules and |
| 44 | reports their results; Play stores the achievements unlocked and the scores posted, ranks them, and serves them to the |
| 45 | game, to the player's account page and to any page that wants a board. |
| 46 | |
| 47 | ## Owns |
| 48 | |
| 49 | - Titles, their achievement definitions and their boards (`play_titles`, `play_achievements`, `play_boards`). |
| 50 | - Unlocks, scores and cloud saves per player (`play_unlocks`, `play_scores`, `play_saves`) and guest adoption. |
| 51 | - The `/api/v1` read and write API. The player pages now live on OpenVibe.Games (achievements, leaderboards, developers); every old page at play.openvibe.network answers 301 to its counterpart there, and the API stays here. |
| 52 | - Request telemetry (`telemetry_samples`, `0003_telemetry.sql`): each HTTP request aggregated by route template, method, status class, |
| 53 | never by raw URL; `/shared`, static assets, health, readiness and `/metrics` are not counted. `server/app.js` builds the collector, |
| 54 | `server/index.js` flushes per key one `http.request` sample plus the active-requests, p95 latency and event-loop-lag gauges each |
| 55 | minute and on stop (openvibe-sdk 0.46, `platform.telemetry-sample@1`), and rows older than 14 days are deleted daily. |
| 56 | |
| 57 | ## Does not own |
| 58 | |
| 59 | - A game's rules, rounds or anti-cheat: the game decides what counts and posts the result. Games' local tables stay in |
| 60 | OpenVibe.Games, which posts to Play. |
| 61 | - People's accounts, names shown elsewhere, or sign-in: OpenVibe.Network. Rewards worn across the network (items) are |
| 62 | OpenVibe.Inventory's; OpenCoins are the Network wallet's. |
| 63 | |
| 64 | ## Depends on |
| 65 | |
| 66 | - **OpenVibe.Network:** service and app tokens (audience `openvibe.play`), the JWKS that verifies them, grants for the |
| 67 | `play.*` capabilities, and person sign-in for "your achievements". |
| 68 | - **OpenVibe.Events:** the account subscriptions (export and deletion), created at boot when |
| 69 | `EVENTS_URL`, `PLAY_EVENTS_SECRET` and the client secret are set. |
| 70 | - **PostgreSQL** (`ov_play`; embedded PGlite in development and tests), Valkey for per-caller limits when configured. |
| 71 | |
| 72 | ## Capabilities |
| 73 | |
| 74 | | Capability | Lets a caller | |
| 75 | |---|---| |
| 76 | | `play.title.manage` | register or update its own title (name, url, summary, achievements, boards) | |
| 77 | | `play.result.write` | post scores and unlocks to its own title, and adopt a guest as a person | |
| 78 | | `play.player.read` | read a player's unlocks in its own title | |
| 79 | |
| 80 | A title belongs to the principal that registered it; another principal's write is refused as if the title did not |
| 81 | exist. Reads of titles and boards are public. |
| 82 | |
| 83 | ## Acceptance |
| 84 | |
| 85 | Tested in `test/` (`npm test`, every file in its own process on PGlite; `npm run test:pg` on PostgreSQL): |
| 86 | `play-api.test.js` (titles, owner-only writes, idempotent scores, best and sum boards with the Monday week, guest |
| 87 | adoption, secret achievements, retired definitions, a person's own achievements, the API), `moved-pages.test.js` |
| 88 | (the page redirects and the crawl files), `time-boards.test.js` |
| 89 | (time boards, their ties, validation and units), `time-migration.test.js` (the kind CHECK widening over existing |
| 90 | boards), `account-data.test.js` |
| 91 | (export and erasure), the auth, limits, redirects, open-redirect, nginx and security tests. |
| 92 | |
| 93 | ## Security |
| 94 | |
| 95 | - Writes need a Network token with the capability, for audience `openvibe.play`; a principal writes only its own titles. |
| 96 | - Every write is idempotent (`ref` for scores, set semantics for unlocks) and limited per calling principal |
| 97 | (`http/caller-limits.js`), sign-in routes per address. |
| 98 | - Boards show a name and a score, never a player id; guests are opaque ids chosen by the game. |
| 99 | - Report vulnerabilities as [SECURITY.md](SECURITY.md) says. |
| 100 | |
| 101 | ## Deploy |
| 102 | |
| 103 | `sudo ovhost deploy play` on the host: unit `openvibe-play.service` on 127.0.0.1:5040, env `/etc/openvibe/play.env`, |
| 104 | database `ov_play`, nginx vhost `deploy/nginx/play.openvibe.network.conf` on the openvibe.network wildcard certificate. |
| 105 | Migrations run at boot. |
| 106 | |
| 107 | ## API (`/api/v1`, problem+json errors) |
| 108 | |
| 109 | | Route | Who | What | |
| 110 | |---|---|---| |
| 111 | | `GET /titles`, `GET /titles/:title` | anyone (CORS) | the games, their achievements (with how many players have each) and boards | |
| 112 | | `GET /titles/:title/boards/:board?period=week\|all&page=` | anyone (CORS) | a board: `rank`, `name`, `score`, `signed_in`, `unit` (`ms` for a time board, `points` otherwise) | |
| 113 | | `GET /people/:subject/achievements` | anyone (CORS) | a person's unlocks in every game, `{ subject, total, titles }`: their profile on OpenVibe.Network and any site's showcase (only people, never a guest) | |
| 114 | | `GET /me/achievements` | a person | their unlocks in every game | |
| 115 | | `PUT /titles/:title` | `play.title.manage` | register or update a title: name, url, summary, achievements, boards | |
| 116 | | `POST /titles/:title/scores` | `play.result.write` | `{ board, ref, entries: [{ player, name, score }] }`, up to 100; a `time` board's score is whole milliseconds from 0 to 86400000 | |
| 117 | | `POST /titles/:title/unlocks` | `play.result.write` | `{ player, achievements: [id] }` → the new ones | |
| 118 | | `POST /titles/:title/players/:guest/adopt` | `play.result.write` | `{ person: "usr_…" }`: a guest signed in | |
| 119 | | `GET /titles/:title/players/:player/achievements` | `play.player.read` | a player's unlocks in your title | |
| 120 | |
| 121 | Tokens are Network service or app tokens for audience `openvibe.play`; the contracts are `play.*` in |
| 122 | [openvibe-contracts](https://github.com/OpenVibers/OpenVibe.Contracts) (0.172.1). |
| 123 | |
| 124 | ## Run |
| 125 | |
| 126 | ```sh |
| 127 | npm install |
| 128 | npm run dev # http://localhost:5040, embedded PGlite in data/pglite |
| 129 | npm test # every test on PGlite; npm run test:pg on PostgreSQL |
| 130 | ``` |
| 131 | |
| 132 | Configuration is environment only (`.env.example`, names only). Production: unit `openvibe-play.service` on |
| 133 | 127.0.0.1:5040, database `ov_play`, nginx vhost `deploy/nginx/play.openvibe.network.conf` on the openvibe.network |
| 134 | wildcard certificate, deployed with `sudo ovhost deploy play`. |
| 135 | |
| 136 | ## Layout |
| 137 | |
| 138 | ``` |
| 139 | server/play/store.js titles, achievements, boards, scores, unlocks, adoption |
| 140 | server/play/account-data.js account export and deletion (ADR-033) |
| 141 | server/http/api.js /api/v1 |
| 142 | server/http/moved.js the old page addresses, 301 to OpenVibe.Games |
| 143 | server/http/discovery.js robots.txt and llms.txt (sitemap.xml and llms-full.txt redirect to Games) |
| 144 | migrations/ the schema (0001 the store, 0002 the retired merges table) |
| 145 | ``` |
| 146 | |
| 147 | ## License |
| 148 | |
| 149 | AGPL-3.0. Part of the [OpenVibe network](https://openvibe.network). |
| 150 | |
| 151 | <!-- versions:start --> |
| 152 | - openvibe-contracts: v0.419.0 |
| 153 | - openvibe-sdk: v0.161.0 |
| 154 | - openvibe-shared: v4.241.0 |
| 155 | <!-- versions:end --> |
| 156 |