openvibers/openvibe.quest README.md
README.md
| 1 | # OpenVibe.Quest |
| 2 | |
| 3 | > Quests across the whole network. |
| 4 | |
| 5 | **Live site:** https://openvibe.quest · **API docs:** https://openvibe.services · **For AI:** https://openvibe.quest/llms.txt |
| 6 | **Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test` |
| 7 | |
| 8 | **Status:** first release. The quest log, the events consumer, the OpenCoins crediting, the item rewards and the pages |
| 9 | are written and tested; it is deployed, and OpenCoins have been credited in production since 2026-10-14 |
| 10 | (`QUEST_COINS=on`, within the daily caps). |
| 11 | **Domain:** `openvibe.quest` · **Port:** 4980 · **Service id:** `quest` · **Env prefix:** `QUEST` |
| 12 | **License:** AGPL-3.0 (same as every OpenVibe service). |
| 13 | |
| 14 | ## What it is |
| 15 | |
| 16 | One quest log across every OpenVibe site. A quest counts something a person really did somewhere in the network — |
| 17 | went live, started a thread, said hello in a public chat room, edited a wiki page — and pays a **badge** on their |
| 18 | profile. When OpenCoins are on it also credits **OpenCoins** through OpenVibe.Network's wallet. OpenCoins are |
| 19 | loyalty points, **never money**, and they are **never Vibes** (Vibes are real money and belong to the sites that |
| 20 | pay them out); nothing here reads or moves money. |
| 21 | |
| 22 | Quest runs no activities of its own. It listens to the domain events the sites already emit and nothing else: no |
| 23 | page views, no clicks, no watch time, and no content a person wrote — not a message's text, not a thread's title. |
| 24 | `/how-it-works` prints the complete list of what is listened to, generated from the same catalog the consumer |
| 25 | subscribes to. |
| 26 | |
| 27 | ## Owns |
| 28 | |
| 29 | OpenVibe.Quest owns the quest log and the catalog that describes it. |
| 30 | |
| 31 | The home page leads with one shared daily quest. A due-based job chooses three verified Events step types across at least two sites, asks OpenVibe.AI to favor recent network changes, and rotates a hand-written set when AI is unavailable. GET `/api/v1/daily` is public; GET `/api/v1/me/daily` returns the caller's private checklist and streak. PUT `/api/v1/me/daily/time-zone` stores an IANA time zone for local-day counting. Apps with `quest.steps.contribute` can POST `/api/v1/step-types`; staff must approve a proposal before the generator can use it. |
| 32 | |
| 33 | `/me` shows progress, a counted Done fold and earned and locked glyph medallions; `/quests` leads with today's quest before the site groups. Both pages render their words and links on the server. Daily quest titles and app badges request a missing glyph from OpenVibe.Design's service draft API with `design.glyph.draft`; pending drawings stay in staff review and the bundled glyph remains visible. Once approved, Quest uses Design's validated SVG image. Set `OV_DESIGN_INTERNAL_URL` for the internal Design service or `QUEST_DESIGN_DRAFTS=off` to disable requests during a rollout. Network must grant the Quest service principal `design.glyph.draft` before enabling draft requests. |
| 34 | |
| 35 | - The **quest catalog** in code ([server/quests/catalog.js](server/quests/catalog.js)): 12 first-party quests, each |
| 36 | with its steps, the events that count them, its badge, its OpenCoins and any item reward, plus the list of events |
| 37 | Quest listens to. A quest may not count an event the catalog does not document; that fails at load. |
| 38 | - `quest_progress` ([migrations/0002_quest.sql](migrations/0002_quest.sql)): one row per (person, quest, step) — how |
| 39 | far they are. |
| 40 | - `quest_completions` ([migrations/0002_quest.sql](migrations/0002_quest.sql), extended by |
| 41 | [migrations/0004_item_rewards.sql](migrations/0004_item_rewards.sql)): one row per (person, quest) — the completion, |
| 42 | its OpenCoins state and its item-grant state. |
| 43 | - `quest_badges` ([migrations/0002_quest.sql](migrations/0002_quest.sql)): one row per (person, badge). |
| 44 | - `quest_event_inbox` ([migrations/0002_quest.sql](migrations/0002_quest.sql)): this service's own receipts of the |
| 45 | OpenVibe.Events deliveries it applied. |
| 46 | - `account_data_events` ([migrations/0003_account_data.sql](migrations/0003_account_data.sql)): its own receipts of |
| 47 | the ADR-033 account export and deletion deliveries. |
| 48 | |
| 49 | It is the authority for a person's quest progress, completions and badges, and for the `quest.*` namespace. Migration |
| 50 | `0001_initial.sql` creates no table. |
| 51 | |
| 52 | ## Does not own |
| 53 | |
| 54 | - **Identity** — the `usr_…` subjects, the Network user id and the sign-in session are OpenVibe.Network's. Quest |
| 55 | stores the subject a delivery names and resolves the wallet id on demand; it issues and verifies no identity itself. |
| 56 | - **Events** — the events and their delivery belong to OpenVibe.Events. Quest runs no activity of its own and only |
| 57 | consumes what Events delivers. |
| 58 | - **Items and their definitions** — the items Quest grants (Sparkle, Hearts, Basic Cap, Fire Name, Ice Name, Rainbow |
| 59 | Name) are OpenVibe.Live's, and the instances are OpenVibe.Inventory's. Quest holds no item row. |
| 60 | - **The wallet and the OpenCoins ledger** — the balance lives in OpenVibe.Network's wallet. Quest holds no balance and |
| 61 | reads none. |
| 62 | - **The sites' content** — threads, posts, comments, chat messages, wiki revisions, streams and job inputs belong to |
| 63 | OpenVibe.Community, OpenVibe.Space, OpenVibe.Chat, OpenVibe.Wiki, OpenVibe.Live and OpenVibe.Tools. Quest counts |
| 64 | that they happened and stores none of them. |
| 65 | |
| 66 | ## Depends on |
| 67 | |
| 68 | | Depends on | For | Env vars | |
| 69 | |---|---|---| |
| 70 | | **OpenVibe.Network** | sign-in with PKCE S256 (OAuth client `quest`), its JWKS, `identity.subject.resolve`, `network.coins.credit`, and the account export/deletion sender | `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL_URL`, `OV_OAUTH_CLIENT_ID`, `OV_OAUTH_CLIENT_SECRET` | |
| 71 | | **OpenVibe.Events** | the deliveries to `POST /internal/events` and the subscriptions it creates at boot | `QUEST_EVENTS_URL`, `QUEST_EVENTS_SECRET`, `QUEST_EVENTS_ENDPOINT` | |
| 72 | | **OpenVibe.Inventory** | granting an item reward, and reading Live's item definitions by alias | `OV_INVENTORY_INTERNAL_URL` | |
| 73 | | **PostgreSQL** | the store (every migration and table) | `DATABASE_URL`, `DATABASE_DIRECT_URL` | |
| 74 | | **Valkey** | shared per-caller limit counters (optional: without it they count in this process) | `VALKEY_URL`, `VALKEY_PREFIX` | |
| 75 | |
| 76 | Packages: **openvibe-contracts** (service auth, capabilities, HTTP problems), **openvibe-sdk** (auth, the events |
| 77 | inbox, account-data, limits, service lifecycle) and **openvibe-shared** (cache policy, metrics, readiness, release, |
| 78 | legal pages). |
| 79 | |
| 80 | ## Capabilities |
| 81 | |
| 82 | The [service manifest](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/manifests/services/quest.json) lists **no capabilities of its |
| 83 | own** and produces no events; no `/api/v1` route is guarded by one ([server/http/principal.js](server/http/principal.js) |
| 84 | `CAPABILITIES` is empty). Quest calls these on other services with its own client-credentials token: |
| 85 | |
| 86 | - `identity.subject.resolve` — OpenVibe.Network `GET /internal/identity/resolve`, to turn a subject into the wallet's |
| 87 | `network_user_id` ([server/quests/coins.js](server/quests/coins.js)). |
| 88 | - `network.coins.credit` — OpenVibe.Network `POST /internal/coins/credit`, to credit a completion's OpenCoins. |
| 89 | - `events.subscription.manage` — OpenVibe.Events, to create the catalog's subscriptions at boot |
| 90 | ([server/events-consumer.js](server/events-consumer.js)). |
| 91 | - `inventory.item.grant` — OpenVibe.Inventory `POST /api/v1/grants`, to give a quest's item |
| 92 | ([server/quests/items.js](server/quests/items.js)). |
| 93 | |
| 94 | ## What works |
| 95 | |
| 96 | | Piece | Where | What it does | |
| 97 | |---|---|---| |
| 98 | | Catalog | [server/quests/catalog.js](server/quests/catalog.js) | 36 first-party quests in code (the 18 of the Live setup series among them), one per real thing a person can do, plus the whole list of events Quest listens to (and the one it does not). A quest may not count an event the catalog does not document: that fails at load | |
| 99 | | Events consumer | [server/events-consumer.js](server/events-consumer.js) | `POST /internal/events`, exactly as OpenVibe.Space does it: raw body, `parseDelivery(…, { requireV2: true })`, the `openvibe-sdk` inbox for once-only application, forwarding headers refused, 503 without `QUEST_EVENTS_SECRET`. It subscribes to the catalog's events at boot (idempotent, off without `QUEST_EVENTS_URL`) | |
| 100 | | What moves a quest | [server/quests/apply.js](server/quests/apply.js) | The person is taken from the field the event's own schema defines (`channel.subject`, `follower`, `author`, `user_subject`, `player.subject`, `owner`; the envelope actor only where the schema says the actor *is* that person). No subject, a `gst_` guest, or a service/app/mod principal → ignored, nothing written | |
| 101 | | Quest log | [server/quests/store.js](server/quests/store.js), [migrations/0002_quest.sql](migrations/0002_quest.sql) | Progress per step, one completion per person per quest (unique), one badge per person, and the Events receipts. Two deliveries of the same event change nothing; so does a replay of the whole request | |
| 102 | | Item rewards | [server/quests/items.js](server/quests/items.js) | Six quests also give one of Live's items: Sparkle, Hearts, Basic Cap, Fire Name, Ice Name and Rainbow Name. Each is granted in OpenVibe.Inventory with Quest's own token (Live names Quest a grantor of those items, ADR-054 §3) as `earned`, keyed by the completion id. Failures and completions from before items are retried by a timer. `QUEST_ITEMS=off` turns it off | |
| 103 | | OpenCoins | [server/quests/coins.js](server/quests/coins.js) | Credits a completion through OpenVibe.Network's wallet with `idempotency_key = <completion id>`. Off by default, per-person and network-wide daily caps, failures recorded with their reason and retried by a timer | |
| 104 | | API | [server/http/api.js](server/http/api.js) | `GET /quests`, `GET /quests/:id`, `GET /me/quests`, `GET /profiles/:subject/badges` (see the table below) | |
| 105 | | Pages | [server/http/pages.js](server/http/pages.js), [server/render/quests.js](server/render/quests.js) | Home (the quests grouped by site, how progress works), a quest, your quest log (plain `<progress>` bars), the rules, the update log — all server-rendered, all readable without JavaScript | |
| 106 | | Discovery | [server/http/discovery.js](server/http/discovery.js) | `robots.txt` (the private log disallowed), `sitemap.xml` (every quest page), `llms.txt`, `llms-full.txt`, JSON-LD | |
| 107 | | Request telemetry | [server/app.js](server/app.js), [server/index.js](server/index.js), [migrations/0006_telemetry.sql](migrations/0006_telemetry.sql) | Every HTTP request is aggregated by route template, method and status class, never by raw URL. Once a minute one `http.request` sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written to this service's own `telemetry_samples` table (openvibe-sdk 0.46 telemetry, the `platform.telemetry-sample@1` shape); rows older than 14 days are deleted daily. `/shared` files, static assets, health, readiness and `/metrics` are not counted. `server/app.js` builds the collector (`createHttpTelemetry`) and mounts its middleware; `server/index.js` flushes it every minute and on stop, and runs the 14-day retention | |
| 108 | | Foundation | `server/app.js`, `server/auth/`, `server/db.js`, `server/observability.js`, `deploy/` | The skeleton's plumbing, unchanged: Network SSO with PKCE, per-caller limits, PostgreSQL migrations, truthful `/api/ready`, nginx and systemd files | |
| 109 | |
| 110 | ## API |
| 111 | |
| 112 | | Route | Who | | |
| 113 | |---|---|---| |
| 114 | | `GET /api/v1/ping` | anyone | `{ ok: true, service: "quest" }` | |
| 115 | | `GET /api/v1/quests` | anyone | the catalog grouped by site (`sites[].quests`), a flat `quests[]`, and whether OpenCoins are live with their caps | |
| 116 | | `GET /api/v1/quests/:id` | anyone | one quest: its steps, its events, its reward | |
| 117 | | `GET /api/v1/me/quests` | a signed-in person | your log: progress per step, completions (with what happened to their coins) and your badges | |
| 118 | | `GET /api/v1/profiles/:subject/badges` | anyone | a person's badges: id, name and date — nothing else, ever | |
| 119 | | `PUT /api/v1/app-quests/:slug` | a developer app (`quest.app_quest.manage`) | create or replace one of the app's quests (`quest.app-quest-request@1` → `quest.app-quest@1`) | |
| 120 | | `GET /api/v1/app-quests` | a developer app (`quest.app_quest.manage`) | the project's app quests, with how many people finished each | |
| 121 | | `DELETE /api/v1/app-quests/:slug` | a developer app (`quest.app_quest.manage`) | retire one: no longer counted or listed; its completions and badges stay | |
| 122 | |
| 123 | Public reads need no token. `/me/quests` takes a person's Network token as a Bearer or this site's session, and |
| 124 | answers only that person's rows. Errors are RFC 9457 `application/problem+json` with a stable `code`. Requests are |
| 125 | counted per caller (a person as themselves, anyone else by address) with the default read numbers — |
| 126 | [server/http/caller-limits.js](server/http/caller-limits.js) `BUDGETS` is empty because no route is expensive yet. |
| 127 | |
| 128 | ## How progress happens |
| 129 | |
| 130 | OpenVibe.Events delivers an event when a site really does something. Quest counts these, and only these: |
| 131 | |
| 132 | | Event | From | The person is | |
| 133 | |---|---|---| |
| 134 | | `live.stream.started` | OpenVibe.Live | the channel's user subject (else the envelope actor) | |
| 135 | | `network.follow.created` | OpenVibe.Network | the follower | |
| 136 | | `community.thread.created` / `community.post.created` / `community.comment.created` | OpenVibe.Community | the author | |
| 137 | | `space.thread.created` | OpenVibe.Space | the author | |
| 138 | | `chat.room.message.created` | OpenVibe.Chat | `payload.user_subject` (public rooms only; a DM never reaches Quest) | |
| 139 | | `wiki.revision.created` | OpenVibe.Wiki | the author (a service edit is ignored) | |
| 140 | | `games.player.joined` | OpenVibe.Games | the player's account subject (a guest is ignored) | |
| 141 | | `tools.job.created` | OpenVibe.Tools | the owner (an anonymous session is ignored) | |
| 142 | | `live.setup.task_completed` | OpenVibe.Live | `payload.user`, the streamer who finished a task on Live's go-live setup list | |
| 143 | | `blog.post.published` | OpenVibe.Blog | the envelope actor (a schedule or a service publishing is nobody) | |
| 144 | | `deals.offer.created` / `deals.vote.changed` | OpenVibe.Deals | the envelope actor: the submitter or the voter (an imported deal's actor is `svc:deals`) | |
| 145 | | `run.job.succeeded` | OpenVibe.Run | `payload.requester` when it is a user (an app's or a service's job is nobody's) | |
| 146 | | `inventory.item.equipped` | OpenVibe.Inventory | `payload.owner` | |
| 147 | | `bot.robot.online` | OpenVibe.Bot | `payload.owner`, the robot's owner (Bot names it since contracts 0.192.0) | |
| 148 | | `games.world.published` | OpenVibe.Games | `payload.owner`, who published a creator world for the first time (contracts 0.194.0) | |
| 149 | | `mediahub.video.published` | OpenVibe.Video (MediaHub) | `payload.owner`, whose upload is public and ready for the first time (contracts 0.196.0) | |
| 150 | | `mediahub.picture.uploaded` | OpenVibe.Pics (MediaHub) | `payload.owner`, who uploaded a picture while signed in (contracts 0.196.0) | |
| 151 | |
| 152 | **App quests** (contracts 0.193.0, [server/quests/app-quests.js](server/quests/app-quests.js)): a developer app defines |
| 153 | quests on its own project's events with an app token (audience `openvibe.quest`, capability `quest.app_quest.manage`). |
| 154 | A step may count only `app.<project_key>.<name>` events of that project, and the person is the event's actor (an app |
| 155 | publishing `on_behalf_of` a user; an app acting as itself is nobody). Only production counts: Events never hands |
| 156 | sandbox events to a first-party consumer, so a sandbox app's quests are its own and never listed. Quest subscribes to |
| 157 | `app.<project_key>.*` when a project's first production quest goes active (and at boot). The reward is a badge named |
| 158 | for the app, never OpenCoins; at most 20 active quests a project; `/apps/quests/<id>` is the public page and the home |
| 159 | page lists them under "Made by apps". |
| 160 | |
| 161 | A step can also name payload values to `match` (`{ task: 'emote' }`): the "Set up your channel" series is one |
| 162 | quest per Live setup task, plus one that counts all 18 tasks. The home page shows a series as one card; its page is |
| 163 | `/series/<id>`. A quest completes when every step reaches its count. The completion, its badge and (when OpenCoins are on) its |
| 164 | coins are recorded once; a redelivery of the same event, or the whole request again, changes nothing. |
| 165 | |
| 166 | ## Data sources and their terms |
| 167 | |
| 168 | | Source | Used for | Terms as applied | |
| 169 | |---|---|---| |
| 170 | | **OpenVibe.Events** (`openvibe.events`) | the domain events that move the log, `POST /internal/events`, loopback only | Signed deliveries only: `X-OpenVibe-Signature-V2` (HMAC-SHA256 over `<timestamp>.<raw body>`), a 300-second replay window, `requireV2`, the secret from `QUEST_EVENTS_SECRET` (32+ characters, comma-separated for rotation). A delivery that carries a forwarding header is refused: nginx never proxies this route. Once-only application is the `openvibe-sdk` inbox, in the same transaction as the writes | |
| 171 | | **openvibe-contracts** v0.190.0 | the shape of every event and of the two Network calls | Every field read is the one the schema at `contracts/events/payloads/<type>.v1.json` defines; no field is inferred and nothing about a person is taken from a name or from the request | |
| 172 | | **OpenVibe.Network** (`openvibe.network`) | resolving a subject to its wallet id, and crediting OpenCoins | `GET /internal/identity/resolve` (capability `identity.subject.resolve`, `identity.resolve-result@1`) gives `network_user_id`; `POST /internal/coins/credit` (capability `network.coins.credit`, body `network.coins-change-request@1`, answer `network.coins-balance-result@1`) credits, with `app_id: quest` — a service token may only name its own app id. Both use this service's own client credentials over the internal URL; no token is logged | |
| 173 | | **OpenVibe sites** (Live, Community, Space, Chat, Wiki, Games, Tools, Network, Blog, Deals, Run, Inventory, Bot) | nothing is fetched from them | Quest makes no outbound request to any site, and no URL from a request ever decides one: the only host it calls is `OV_NETWORK_INTERNAL_URL` | |
| 174 | |
| 175 | Rewards are badges and OpenCoins. Nothing here is money, and nothing here is Vibes. |
| 176 | |
| 177 | ## Configuration |
| 178 | |
| 179 | See [.env.example](.env.example) for every name. Required in production: `OV_OAUTH_CLIENT_SECRET`, `BASE_URL`, |
| 180 | `DATABASE_URL` and `DATABASE_DIRECT_URL`. The product adds: |
| 181 | |
| 182 | | Variable | Default | What it does | |
| 183 | |---|---|---| |
| 184 | | `QUEST_EVENTS_SECRET` | unset | signs the Events deliveries (32+ characters; comma-separated for rotation). Unset turns the consumer off (503) and the quest log then never moves | |
| 185 | | `QUEST_EVENTS_URL` | unset | OpenVibe.Events' base URL, used once at boot to create the subscriptions for the catalog's events | |
| 186 | | `QUEST_EVENTS_ENDPOINT` | `http://127.0.0.1:<port>/internal/events` | the endpoint the subscriptions name | |
| 187 | | `QUEST_COINS` | `off` | `on` credits rewards through Network's wallet; `off` records completions with `coins_state=skipped` and the pages say the rewards start soon | |
| 188 | | `QUEST_COINS_DAILY_CAP` | `50` | OpenCoins per person per UTC day; over it a completion is skipped with the reason | |
| 189 | | `QUEST_COINS_GLOBAL_DAILY_CAP` | `5000` | OpenCoins for everyone together per UTC day | |
| 190 | | `QUEST_COINS_RETRY_MS` | `300000` | how often a failed or unfinished credit is retried (only while `QUEST_COINS=on`) | |
| 191 | |
| 192 | ## Development |
| 193 | |
| 194 | ```bash |
| 195 | npm install |
| 196 | fnm exec --using=22 npm test # every test/*.test.js, on temp PGlite databases with a mock Network |
| 197 | fnm exec --using=22 npm run dev # http://localhost:4980 |
| 198 | ``` |
| 199 | |
| 200 | Without `DATABASE_URL` development uses an embedded PGlite database in `data/pglite` (one process only). `npm run |
| 201 | test:pg` runs the same suite through PostgreSQL and PgBouncer (see [.github/workflows/ci.yml](.github/workflows/ci.yml)). |
| 202 | |
| 203 | The mock Network in [test/helpers/mocks.js](test/helpers/mocks.js) also serves `/internal/identity/resolve` and |
| 204 | `/internal/coins/credit`, checked as Network checks them (a `svc:quest` token with the capability, its own app id, |
| 205 | idempotent by key), so the OpenCoins tests need no real wallet. |
| 206 | |
| 207 | ## Acceptance |
| 208 | |
| 209 | `npm test` ([test/run.js](test/run.js)) runs every `test/*.test.js` in its own process on temp PGlite databases and |
| 210 | an in-process mock of OpenVibe.Network ([test/helpers/mocks.js](test/helpers/mocks.js)); no network or running site is |
| 211 | needed. `npm run test:pg` runs the same suite through PostgreSQL and PgBouncer. The main proofs: |
| 212 | |
| 213 | - [test/events-consumer.test.js](test/events-consumer.test.js) — a bad signature, a forwarded request and an |
| 214 | unconfigured secret are refused; the same event twice changes nothing; progress counts to completion and the badge |
| 215 | is awarded once; an event that names nobody creditable is ignored; OpenCoins off is skipped, on credits with the |
| 216 | completion id as the idempotency key, the two daily caps stop it, and a failure is recorded and retried. |
| 217 | - [test/quests-api.test.js](test/quests-api.test.js) — the catalog and a quest are public; `/me/quests` requires |
| 218 | sign-in and shows only the caller's rows; the badge endpoint answers an id, a name and a date and nothing else; the |
| 219 | pages are complete without JavaScript; a hostile title is escaped. |
| 220 | - [test/items.test.js](test/items.test.js) — a quest's item is granted in a stand-in Inventory with `origin: earned` |
| 221 | and the completion id, a refusal is recorded and retried, a completion from before items is granted by the retry, |
| 222 | and `QUEST_ITEMS=off` grants nothing. |
| 223 | - [test/account-data.test.js](test/account-data.test.js) — an export carries only the person's own progress, |
| 224 | completions and badges; a deletion erases them whole and confirms the counts; a redelivery changes nothing. |
| 225 | - [test/security-secrets.test.js](test/security-secrets.test.js) — the OAuth client secret never appears in a |
| 226 | response, the database or a log line, over every route and hostile input. |
| 227 | - [test/security-session.test.js](test/security-session.test.js) — a FedCM assertion, app, service or internal token |
| 228 | is never accepted as a session cookie. |
| 229 | - [test/caller-limits.test.js](test/caller-limits.test.js) — past its budget a caller gets 429 problem+json with |
| 230 | Retry-After before the route does any work, while another caller still passes. |
| 231 | - [test/open-redirect.test.js](test/open-redirect.test.js) — the sign-in `next` never leaves the site. |
| 232 | - [test/no-internal-key.test.js](test/no-internal-key.test.js) — no `X-Internal-Key` anywhere in the code, deploy |
| 233 | files or the running service (ADR-014). |
| 234 | - [test/auth-ops.test.js](test/auth-ops.test.js) — PKCE S256 sign-in, truthful `/api/ready`, `/release.json`, and |
| 235 | loopback-only `/metrics`. |
| 236 | - [test/auth-jwks.test.js](test/auth-jwks.test.js) — the Network signing key is kept across outages, a rotation is |
| 237 | honoured at once and unknown-kid floods are throttled. |
| 238 | - [test/discovery.test.js](test/discovery.test.js) — `robots.txt`, `sitemap.xml`, `llms.txt`, `llms-full.txt` and the |
| 239 | JSON-LD carry real public entries only and take `lastmod` from STATUS.json, not the clock. |
| 240 | - [test/service-kit.test.js](test/service-kit.test.js) — the graceful stop runs its close steps in the manifest's |
| 241 | order and resolves exit 0. |
| 242 | - [test/layout.test.js](test/layout.test.js), [test/asset-cache.test.js](test/asset-cache.test.js), |
| 243 | [test/nginx-auth-limit.test.js](test/nginx-auth-limit.test.js) and [test/perf-budget.test.js](test/perf-budget.test.js) |
| 244 | — the shared boost marker and the asset cache rules, the nginx limit zones, and the home page's size budgets. |
| 245 | |
| 246 | ## Deploy (for the lead) |
| 247 | |
| 248 | - **Deploy:** `sudo ovhost deploy quest` on the host (git checkout at `/opt/openvibe.quest`, unit |
| 249 | `openvibe-quest.service` on 127.0.0.1:4980, env `/etc/openvibe/quest.env`, database `ov_quest` on the data role). |
| 250 | - **nginx:** [deploy/nginx/openvibe.quest.conf](deploy/nginx/openvibe.quest.conf), installed with `ov-vhost-install`. |
| 251 | `/internal/` is answered 404 from outside; Events posts to the loopback address directly. |
| 252 | - **Rollback:** ovhost puts the previous sha back by itself when `/api/ready` does not answer after the restart. |
| 253 | - **Before OpenCoins can be turned on**, OpenVibe.Network must grant the `quest` OAuth client |
| 254 | `identity.subject.resolve` and `network.coins.credit` (its service-principal grants), and `QUEST_EVENTS_SECRET` |
| 255 | and a subscription for the catalog's events must exist at OpenVibe.Events. |
| 256 | - Register the service and its capabilities in **OpenVibe.Contracts** (`contracts-service: quest` in CI) and with |
| 257 | **OpenVibe.Services** before the first deploy. |
| 258 | |
| 259 | ## Account export and deletion |
| 260 | |
| 261 | A person's account at OpenVibe.Network can be exported and deleted, and every service holding their rows answers its |
| 262 | part (ADR-033). Quest receives `network.account.export_requested` and `network.account.deleted` at `POST /internal/events` |
| 263 | (loopback only) through the consumer it already runs — the two account topics are handed to |
| 264 | [server/identity/account-data.js](server/identity/account-data.js), which maps the tables, and the boot-time |
| 265 | subscriptions for them are created (alongside the catalog's events) by |
| 266 | [server/events-consumer.js](server/events-consumer.js): |
| 267 | |
| 268 | - **Exported:** the quests a person is partway through (`progress.json`), the quests they completed and their OpenCoins |
| 269 | (`completions.json`) and the badges they earned (`badges.json`), pushed to `POST /internal/account-exports/:id/parts` |
| 270 | with this service's own token. Nothing here is a secret — Quest stores no token, key or credential. |
| 271 | - **Erased:** all three tables hold the person's own rows and nothing anyone else's page hangs under them, so they are |
| 272 | deleted whole and nothing is kept. Deleting a completion deletes its OpenCoins record here; the wallet credit at |
| 273 | OpenVibe.Network is Network's own data. Quest then confirms with `POST /internal/account-deletions/:id/confirmations` |
| 274 | and the counts. |
| 275 | - **Anonymized:** nothing. There is no row Quest keeps that was written by this person for another person to read. |
| 276 | |
| 277 | The delivery receipts (`quest_event_inbox`) are this service's own, not a person's rows, and are neither exported nor |
| 278 | erased. |
| 279 | |
| 280 | Environment: `QUEST_EVENTS_SECRET` (comma-separated for rotation, 32+ characters each; unset makes the route answer |
| 281 | 503), `QUEST_EVENTS_URL` (or `EVENTS_URL`) is where the subscriptions are created at boot (off when unset), and |
| 282 | `QUEST_EVENTS_ENDPOINT` overrides the loopback endpoint; `QUEST_EVENTS_SUBSCRIBE=0` turns the boot-time subscription off. |
| 283 | |
| 284 | ## Security (threat notes) |
| 285 | |
| 286 | Reporting a vulnerability: [SECURITY.md](SECURITY.md). |
| 287 | |
| 288 | - Session tokens are httpOnly cookies; a FedCM assertion or an app or service token is never a session. |
| 289 | - Secrets live only in the env file; only environment variable names appear in code and docs, and no secret is |
| 290 | logged. A wallet or identity failure is recorded as Network's own short code, never as a token or a body. |
| 291 | - Request bodies are never logged. The Events route is the one place a body is read raw, and only to verify its |
| 292 | signature. |
| 293 | - Nothing in a request may decide a URL this service fetches: the only host Quest calls is the configured |
| 294 | `OV_NETWORK_INTERNAL_URL`. |
| 295 | - A person's log is theirs: `/me/quests` answers their rows only, and the public badge endpoint answers an id, a |
| 296 | name and a date and nothing else. |
| 297 | |
| 298 | --- |
| 299 | |
| 300 | Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers). |
| 301 | |
| 302 | <!-- versions:start --> |
| 303 | - openvibe-contracts: v0.419.0 |
| 304 | - openvibe-sdk: v0.168.0 |
| 305 | - openvibe-shared: v4.248.0 |
| 306 | - openvibe-publishing: v1.7.0 |
| 307 | <!-- versions:end --> |
| 308 |