openvibers/openvibe.play AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Play |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Play is the network's game services: achievements, leaderboards and cloud saves for every OpenVibe game. A |
| 6 | game registers its **title** with its own OpenVibe token, posts what its players did, and anyone reads its boards. The |
| 7 | rules of each game stay in the game; Play keeps the record and answers it. |
| 8 | |
| 9 | - **Domain:** `play.openvibe.network` · **Port:** 5040 · **Service id:** `play` · **Env prefix:** `PLAY` |
| 10 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey for |
| 11 | shared limits when configured. |
| 12 | - **Read first:** [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example). |
| 13 | |
| 14 | **Network context:** accounts and sign-in are OpenVibe.Network (service/app tokens with audience `openvibe.play`, the |
| 15 | JWKS, `play.*` capability grants, and person sign-in for "your achievements"). Items are OpenVibe.Inventory's and |
| 16 | OpenCoins are the wallet's — Play does not grant either. Account export/deletion arrives from OpenVibe.Events at |
| 17 | `/internal/events`. Game rules, rounds, anti-cheat and the player pages live in OpenVibe.Games; Play is the record and |
| 18 | the API. |
| 19 | |
| 20 | ## Commands |
| 21 | |
| 22 | ```bash |
| 23 | npm ci # install (contracts/sdk/shared pinned by tag) |
| 24 | npm run dev # http://localhost:5040, embedded PGlite in data/pglite |
| 25 | npm start # node server/index.js |
| 26 | npm test # every test/*.test.js on PGlite with a mock Network, no network |
| 27 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 28 | npm test -- play-api # only test files whose name contains the word |
| 29 | node --check <file.js> # syntax check (no linter configured) |
| 30 | ``` |
| 31 | |
| 32 | No build step and no public pages: Play's own CSS is [public/css/](public/css/), but the player-facing pages were |
| 33 | moved to OpenVibe.Games. The old addresses (`/`, `/t/:title`, `/me`, `/docs`, `/updates`) answer 301 there through |
| 34 | [server/http/moved.js](server/http/moved.js). |
| 35 | |
| 36 | ## Architecture at a Glance |
| 37 | |
| 38 | - **Entry:** [server/index.js](server/index.js) — listener, telemetry retention and the two account subscriptions; |
| 39 | graceful stop in `createLifecycle()`. |
| 40 | - **App factory:** [server/app.js](server/app.js) — helmet/CSP, SSO, `/auth`, `/internal/events`, `/shared` + static, |
| 41 | `/api/v1`, the moved-page redirects, discovery, 404/error handler. Tests call `createApp({ store, fetchImpl, ... })`. |
| 42 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production `/etc/openvibe/play.env`. |
| 43 | - **Store:** [server/play/store.js](server/play/store.js) — titles, achievements, boards, scores, unlocks, guest |
| 44 | adoption, sandbox restrictions, cloud saves. Account data: |
| 45 | [server/play/account-data.js](server/play/account-data.js). |
| 46 | - **HTTP:** [server/http/api.js](server/http/api.js) (`/api/v1`), [server/http/principal.js](server/http/principal.js) |
| 47 | (service/app/person, title ownership, capabilities), [server/http/moved.js](server/http/moved.js), |
| 48 | [server/http/discovery.js](server/http/discovery.js) (robots/llms; sitemap and llms-full redirect to Games), |
| 49 | [server/http/router.js](server/http/router.js) (async router), [server/http/caller-limits.js](server/http/caller-limits.js). |
| 50 | - **Auth:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js). |
| 51 | - **Rendering:** [server/render/](server/render/) (`html.js`, `layout.js`, `markdown.js`) only for the error/redirect |
| 52 | surfaces and discovery; there is no site. |
| 53 | - **Database:** [server/db.js](server/db.js) and [migrations/](migrations/): `play_titles`, `play_achievements`, |
| 54 | `play_boards`, `play_unlocks`, `play_scores`, `play_saves`, `play_subject_merges`, `account_data_events`, |
| 55 | `telemetry_samples`. |
| 56 | - **Deploy:** [deploy/systemd/openvibe-play.service](deploy/systemd/openvibe-play.service) (`/etc/openvibe/play.env`, |
| 57 | `/opt/play.openvibe.network`) and [deploy/nginx/play.openvibe.network.conf](deploy/nginx/play.openvibe.network.conf) |
| 58 | on the openvibe.network wildcard certificate. `sudo ovhost deploy play`; database `ov_play`; migrations run at boot. |
| 59 | |
| 60 | ## Ownership and Boundaries |
| 61 | |
| 62 | Play owns titles, their achievement definitions, boards, scores, unlocks, guest→person adoption and cloud saves per |
| 63 | player, plus request telemetry and account-export/deletion receipts. It does **not** own a game's rules, rounds or |
| 64 | anti-cheat (the game decides what counts and posts the result), accounts or names (Network), wearable rewards |
| 65 | (Inventory), OpenCoins (Network wallet), event delivery (Events), or the host/database/Valkey (Host). A title belongs |
| 66 | to the principal that registered it: another principal's write is refused exactly as if the title did not exist. |
| 67 | |
| 68 | ## Auth and Capabilities |
| 69 | |
| 70 | | Capability | Lets a caller | |
| 71 | |---|---| |
| 72 | | `play.title.manage` | register or update its own title (name, url, summary, achievements, boards), delete a sandbox title | |
| 73 | | `play.result.write` | post scores/unlocks and cloud saves to its own title, adopt a guest as a person | |
| 74 | | `play.player.read` | read a player's unlocks/saves in its own title | |
| 75 | |
| 76 | Tokens are Network service or app tokens for audience `openvibe.play`; person routes accept the person's token or this |
| 77 | site's session. Public title/board/profile reads answer every origin (CORS) and require no token; writes never. |
| 78 | |
| 79 | - **Sandbox apps** (ADR-014): a developer's sandbox app registers up to 3 titles with ids starting `sandbox-`, guest |
| 80 | players only, never listed/counted/indexed, readable at their id with `sandbox: true`, and deletable by their app. |
| 81 | A production app registers the real name and never takes a `sandbox-` id; a production title is never deleted. |
| 82 | - Guest adoption (`POST /titles/:title/players/:guest/adopt`) links a guest's record to a `usr_…`; it must be |
| 83 | idempotent and audited as that title's principal. |
| 84 | - Errors are RFC 9457 problem+json. Keep the "not yours = not found" rule: never leak another principal's title. |
| 85 | |
| 86 | ## API (`/api/v1`) |
| 87 | |
| 88 | Anyone (CORS): `GET /titles`, `GET /titles/:title`, `GET /titles/:title/boards/:board?period=week|all&page=`, |
| 89 | `GET /people/:subject/achievements`. A person: `GET /me/achievements`, `GET /me/saves`, |
| 90 | `DELETE /me/saves/:title/:slot`. A title's principal: `PUT /titles/:title`, `DELETE /titles/:title` (sandbox only), |
| 91 | `POST /titles/:title/scores` (up to 100 entries; a `time` board's score is whole ms 0–86400000), |
| 92 | `POST /titles/:title/unlocks`, `POST /titles/:title/players/:guest/adopt`, |
| 93 | `GET|PUT|DELETE /titles/:title/players/:player/saves[/:slot]` (a stale `version` is 409; list omits the data), |
| 94 | `GET /titles/:title/players/:player/achievements` (`play.player.read`). Machine endpoints `/api/health`, `/api/ready`, |
| 95 | `/release.json`, `/metrics` are loopback only. |
| 96 | |
| 97 | ## Conventions |
| 98 | |
| 99 | - CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through |
| 100 | [server/http/router.js](server/http/router.js). |
| 101 | - New schema = a new `migrations/NNNN_name.sql`; never edit one that has run. Keep `play_` prefixes. |
| 102 | - Public reads are CORS-open and credential-free; a presented token must still hold the route's capability. |
| 103 | - IDs are stable strings from the caller (`title`, board, achievement id, guest/player id); never derive authority |
| 104 | from a name. A hidden achievement's name is returned only after unlock. |
| 105 | - Phone first and no Font Awesome apply to any page that still renders here (error/redirect/discovery only); the |
| 106 | player pages are Games' responsibility now. |
| 107 | - "Calm reduces, only Off removes": a board `period` or a hidden definition reduces; only an explicit delete removes |
| 108 | (and a production title has no delete). |
| 109 | - Never log tokens, a player's record or a save's data. |
| 110 | |
| 111 | ## Key Pitfalls |
| 112 | |
| 113 | - **Moved pages:** Play renders no site; `/`, `/t/:title`, `/me`, `/docs`, `/updates` must 301 to the matching |
| 114 | OpenVibe.Games page. Adding a page here would fork the product — add it to Games instead. |
| 115 | - **Sandbox containment:** sandbox titles are invisible to listings, profiles, counts and Search, cannot adopt a |
| 116 | person's record, and cannot use a real prefix. [test/sandbox.test.js](test/sandbox.test.js) guards it. |
| 117 | - **Time boards:** a time board's unit is `ms`, scores are whole milliseconds in 0…86 400 000; do not accept a float, |
| 118 | seconds or a negative. [test/time-boards.test.js](test/time-boards.test.js) and the time migration guard it. |
| 119 | - **Cloud saves:** a write with a stale version is 409; the list endpoint never returns `data`; a save belongs to the |
| 120 | title's principal for its own players and to the person for `/me/saves`. `cloud saves` are not achievements. |
| 121 | - **Guest adoption** must merge only that title's guest record, never another title's, and must be repeatable. |
| 122 | - **Account export/deletion** is loopback-only, signature-verified and idempotent; a redelivery changes nothing. |
| 123 | - **Drill/readiness:** the DB is the required readiness check; Network JWKS, OAuth client and Valkey are optional and |
| 124 | reported per check on `/api/ready`. |
| 125 | |
| 126 | ## Testing |
| 127 | |
| 128 | `npm test` runs each file in its own process on temp PGlite with a mock Network: API, sandbox, saves, time boards and |
| 129 | their migration, moved pages, caller limits, auth/JWKS, discovery, secret-leak/session/redirect, service kit and |
| 130 | telemetry. `npm run test:pg` for the PostgreSQL path. Run the narrowest file first, then the suite; always |
| 131 | `node --check` changed files. |
| 132 | |
| 133 | ## Documentation |
| 134 | |
| 135 | - [README.md](README.md) — purpose, ownership, capabilities, acceptance, security, deploy, API, layout. |
| 136 | - [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list. |
| 137 | - [.env.example](.env.example) — variable names only. |
| 138 | - [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md). |
| 139 |