openvibers/openvibe.quest AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Quest |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Quest is one quest log across every OpenVibe site: it counts something a person really did (went live, |
| 6 | started a thread, said hello in a public chat, edited a wiki page, played a game) and pays a badge. When OpenCoins are |
| 7 | on it credits loyalty points through OpenVibe.Network's wallet (never money, never Vibes), and it can grant one of |
| 8 | Live's items through OpenVibe.Inventory. The home page leads with one shared daily quest; `/me` and `/me/daily` show a |
| 9 | private checklist, streak and level. |
| 10 | |
| 11 | - **Domain:** `openvibe.quest` · **Port:** 4980 · **Service id:** `quest` · **Env prefix:** `QUEST` |
| 12 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey for |
| 13 | shared limits when configured. |
| 14 | - **Read first:** [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example). |
| 15 | |
| 16 | **Network context:** identity/sign-in and the wallet are OpenVibe.Network (OAuth client `quest`, PKCE S256; staff |
| 17 | roles). Events and their delivery are OpenVibe.Events. Item instances are OpenVibe.Inventory (Live names Quest a |
| 18 | grantor of its items, ADR-054 §3). Platform experience/levels are `openvibe-sdk/levels` calling Network. Daily quest |
| 19 | glyphs may request a draft from OpenVibe.Design (`design.glyph.draft`; with a bundled fallback always visible). Quest |
| 20 | stores no identity, no event, no item row and no balance. |
| 21 | |
| 22 | ## Commands |
| 23 | |
| 24 | ```bash |
| 25 | npm ci # install (contracts/sdk/shared pinned by tag) |
| 26 | npm run dev # http://localhost:4980, PGlite under data/ |
| 27 | npm start # node server/index.js |
| 28 | npm test # every test/*.test.js on temp PGlite, mock Network, no network |
| 29 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 30 | npm test -- daily # only test files whose name contains the word |
| 31 | node --check <file.js> # syntax check (no linter configured) |
| 32 | ``` |
| 33 | |
| 34 | No build step: pages are server-rendered; CSS/JS under [public/css/](public/css/) (`app.css`, `quest-visual.css`) and |
| 35 | [public/js/quest-visual.js](public/js/quest-visual.js). |
| 36 | |
| 37 | ## Architecture at a Glance |
| 38 | |
| 39 | - **Entry:** [server/index.js](server/index.js) — listener, telemetry, Events subscriptions, the OpenCoins/item/level |
| 40 | retry timers and graceful stop. `QUEST_DRILL` keeps boot-time work off. |
| 41 | - **App factory:** [server/app.js](server/app.js) — CSP, SSO, `/auth`, `/internal/events`, `/shared`+static, `/api/v1`, |
| 42 | discovery/agent twins, pages, 404/error handler. |
| 43 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production `/etc/openvibe/quest.env`. |
| 44 | - **Catalog:** [server/quests/catalog.js](server/quests/catalog.js) — 36 first-party quests and every event Quest |
| 45 | listens to; a quest naming an undocumented event fails at load. Never add an event without the catalog entry. |
| 46 | - **Progress and rewards:** [server/quests/apply.js](server/quests/apply.js) (which event field names the person), |
| 47 | [server/quests/store.js](server/quests/store.js), [server/quests/daily.js](server/quests/daily.js), |
| 48 | [server/quests/coins.js](server/quests/coins.js), [server/quests/items.js](server/quests/items.js), |
| 49 | [server/quests/levels.js](server/quests/levels.js), [server/quests/app-quests.js](server/quests/app-quests.js), |
| 50 | [server/quests/glyph-drafts.js](server/quests/glyph-drafts.js). |
| 51 | - **Events:** [server/events-consumer.js](server/events-consumer.js) — raw body, v2 signature required, forwarded |
| 52 | headers refused, SDK inbox for once-only application, 503 without `QUEST_EVENTS_SECRET`. |
| 53 | - **HTTP:** [server/http/api.js](server/http/api.js), [server/http/pages.js](server/http/pages.js), |
| 54 | [server/http/principal.js](server/http/principal.js), [server/http/discovery.js](server/http/discovery.js), |
| 55 | [server/http/router.js](server/http/router.js), [server/http/caller-limits.js](server/http/caller-limits.js). |
| 56 | - **Auth/account data:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js), |
| 57 | [server/identity/account-data.js](server/identity/account-data.js), [server/identity/test-subjects.js](server/identity/test-subjects.js). |
| 58 | - **Rendering:** [server/render/](server/render/) — `layout.js`, `quests.js`, `quest-icons.js`, `badge-art.js`, |
| 59 | `html.js`, `short.js`, `markdown.js`; agent twins [server/agent-pages.js](server/agent-pages.js), |
| 60 | [server/agent-descriptor.js](server/agent-descriptor.js). Search documents: [server/search-index.js](server/search-index.js). |
| 61 | - **Database:** [server/db.js](server/db.js) and [migrations/](migrations/): `quest_progress`, `quest_completions`, |
| 62 | `quest_badges`, `quest_event_inbox`, `account_data_events`, `quest_index_revisions`, `app_quests`, |
| 63 | `quest_subject_merges`, `daily_quests`, `daily_step_types`, `daily_events`, `daily_personal_quests`, |
| 64 | `daily_completions`, `daily_preferences`, `channel_daily_quests`, `daily_ai_choices`, a `test_subject` fixture |
| 65 | table and `telemetry_samples`. |
| 66 | - **Deploy:** [deploy/systemd/openvibe-quest.service](deploy/systemd/openvibe-quest.service) (127.0.0.1:4980, |
| 67 | `/etc/openvibe/quest.env`, `/opt/openvibe.quest`) and [deploy/nginx/openvibe.quest.conf](deploy/nginx/openvibe.quest.conf) |
| 68 | (`/internal/` is 404 from outside). `sudo ovhost deploy quest`; database `ov_quest`. |
| 69 | |
| 70 | ## Ownership and Boundaries |
| 71 | |
| 72 | Quest is the authority for a person's progress, completions, badges, daily quests/choices/preferences and app quests, |
| 73 | and for the `quest.*` namespace. It does **not** own identity, the wallet/OpenCoins ledger, Events, item definitions |
| 74 | or instances, or the sites' content (threads, posts, comments, messages, revisions, streams). Quest counts that an |
| 75 | event happened and stores none of the content itself. A `gst_` guest, a service/app/mod principal, or an event with |
| 76 | no person field is ignored — nothing is written. |
| 77 | |
| 78 | ## Auth and Capabilities |
| 79 | |
| 80 | - A person is the Network token or this site's session (cookie writes must be same-origin). The only capabilities |
| 81 | Quest itself uses are `quest.app_quest.manage` (a developer app defines its own quests), |
| 82 | `quest.steps.contribute` (an app proposes a step type) and `quest.steps.review` (staff). `CAPABILITIES` in |
| 83 | [server/http/principal.js](server/http/principal.js) is exactly that list — add to it and Contracts together. |
| 84 | - Outbound with Quest's own client-credentials token: `identity.subject.resolve`, `network.coins.credit`, |
| 85 | `events.subscription.manage`, `inventory.item.grant`, and `network.levels.award` for platform experience. |
| 86 | `design.glyph.draft` only when the Design internal URL is set; `QUEST_DESIGN_DRAFTS=off` disables requests. |
| 87 | - OpenCoins: `QUEST_COINS=on` credits with `idempotency_key = <completion id>`; per-person and global daily caps put |
| 88 | over-cap completions in `coins_state=skipped` with a reason. Off records `skipped` and the pages say rewards start |
| 89 | soon. |
| 90 | - Item rewards: six quests grant one of Live's items as `earned`, keyed by the completion id; `QUEST_ITEMS=off` turns |
| 91 | it off. A refusal is recorded and retried by a timer, never silently dropped. |
| 92 | - Developer app quests cannot mint platform experience from arbitrary app events; their experience state is |
| 93 | `skipped`. |
| 94 | - Errors are RFC 9457 problem+json with stable `quest.*` codes; limits return 429 with `Retry-After` before the route |
| 95 | does work. |
| 96 | |
| 97 | ## API (`/api/v1`) |
| 98 | |
| 99 | Public: `GET /ping`, `/quests`, `/quests/:id`, `/daily`, `/daily/:date`, `/step-types`, |
| 100 | `/profiles/:subject/badges` (id, name, date only), `/profiles/:subject/streak`, `/channels/:subject/quest`. |
| 101 | Signed in: `GET /me/quests`, `GET /me/daily`, `GET /quests/today`, `GET /quests/today/steps/:id`, |
| 102 | `POST /quests/today/steps/:id/done`, `PUT /me/daily/ai-text`, `PUT /me/daily/time-zone`, `PUT /me/channel-quest`. |
| 103 | App: `GET|PUT|DELETE /app-quests[/:slug]`, `POST /step-types`; staff `POST /step-types/:id/review`. |
| 104 | Machine endpoints `/api/health`, `/api/ready`, `/release.json`, `/metrics` are loopback only. |
| 105 | |
| 106 | ## Conventions |
| 107 | |
| 108 | - CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through the router. |
| 109 | - **The person is not the envelope actor.** Take it from the field the event's schema defines (`channel.subject`, |
| 110 | `follower`, `author`, `user_subject`, `player.subject`, `owner`), and only use the actor where the schema says the |
| 111 | actor is that person. |
| 112 | - Two deliveries of one event change nothing; a replay of the whole request changes nothing (SDK inbox + unique |
| 113 | completion). |
| 114 | - New schema = a new `migrations/NNNN_name.sql`; never edit one that has run. Keep `quest_`/`daily_` prefixes. |
| 115 | - Daily quests: one per UTC date, progress uses the person's chosen IANA time zone; a due-based job picks three |
| 116 | verified step types across at least two sites, asks AI for recent network changes, and falls back to a hand-written |
| 117 | set when AI is unavailable. |
| 118 | - Glyphs: the bundled glyph is always the fallback; only a Design-approved SVG may replace it, and pending drafts |
| 119 | never leave Design. |
| 120 | - CSP: add origins to `helmet` in [server/app.js](server/app.js); do not widen `frameAncestors`/`objectSrc`. |
| 121 | - Phone first: home, a quest, `/me`, `/me/daily` fit 390 px; `<progress>` bars and forms work without JS. |
| 122 | - Empty states use the shared kit; no Font Awesome (shared glyphs only); "Calm reduces, only Off removes". |
| 123 | |
| 124 | ## Key Pitfalls |
| 125 | |
| 126 | - **Event signatures:** forwarded requests and a missing `QUEST_EVENTS_SECRET` are refusals (503), not a bypass. A |
| 127 | bad signature writes nothing. Never log an event's content. |
| 128 | - **Catalog load:** a quest that counts an undocumented event fails at load — add the event to the catalog and the |
| 129 | subscription list, then Contracts if needed. |
| 130 | - **Badges are public and minimal:** `GET /profiles/:subject/badges` returns id, name and date and nothing else. |
| 131 | - **OpenCoins are loyalty points, not money or Vibes.** Never read or move money; never credit without the idempotency |
| 132 | key, and never exceed either daily cap. |
| 133 | - **Levels:** only catalog/daily completions may award platform experience; a developer app quest sets |
| 134 | `experience_state=skipped`. A 409 `levels.duplicate` is success, 404 `levels.person_not_found` is a skip. |
| 135 | - **Drill mode** (`QUEST_DRILL`) keeps subscriptions and every retry timer off; add new timers there. |
| 136 | - **Account export/deletion** runs through `/internal/events` only, is idempotent, and carries only the person's rows; |
| 137 | a deletion erases them whole. |
| 138 | - **Migration 0009** adds the `event_outbox` rejected partial index; keep it. |
| 139 | |
| 140 | ## Testing |
| 141 | |
| 142 | `npm test` runs each file in its own process on temp PGlite with a mock Network: events consumer (signature, replay, |
| 143 | per-event person field), quests API/pages, daily and streaks, levels, items, coins off/on and caps, app quests, step |
| 144 | types, account data, discovery, SSO/JWKS, caller limits, secret-leak/session/redirect, service kit, layout and perf |
| 145 | budgets. `npm run test:pg` for PostgreSQL. Run the narrowest file first, then the suite; always `node --check` |
| 146 | changed files. |
| 147 | |
| 148 | ## Documentation |
| 149 | |
| 150 | - [README.md](README.md) — what it is, owns/does-not-own, capabilities/grants, what works, API, how progress happens, |
| 151 | data terms, configuration, acceptance, deploy, account data, security. |
| 152 | - [docs/screenshots/](docs/screenshots/) — recorded UI states. |
| 153 | - [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list. |
| 154 | - [.env.example](.env.example), [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md). |
| 155 |