openvibers/openvibe.quest README.md

README.md

main 26.6 KB · 308 lines · markdown

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
9are 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
16One quest log across every OpenVibe site. A quest counts something a person really did somewhere in the network —
17went live, started a thread, said hello in a public chat room, edited a wiki page — and pays a **badge** on their
18profile. When OpenCoins are on it also credits **OpenCoins** through OpenVibe.Network's wallet. OpenCoins are
19loyalty points, **never money**, and they are **never Vibes** (Vibes are real money and belong to the sites that
20pay them out); nothing here reads or moves money.
21
22Quest runs no activities of its own. It listens to the domain events the sites already emit and nothing else: no
23page 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
25subscribes to.
26
27## Owns
28
29OpenVibe.Quest owns the quest log and the catalog that describes it.
30
31The 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
49It 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
76Packages: **openvibe-contracts** (service auth, capabilities, HTTP problems), **openvibe-sdk** (auth, the events
77inbox, account-data, limits, service lifecycle) and **openvibe-shared** (cache policy, metrics, readiness, release,
78legal pages).
79
80## Capabilities
81
82The [service manifest](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/manifests/services/quest.json) lists **no capabilities of its
83own** 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
123Public reads need no token. `/me/quests` takes a person's Network token as a Bearer or this site's session, and
124answers only that person's rows. Errors are RFC 9457 `application/problem+json` with a stable `code`. Requests are
125counted 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
130OpenVibe.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
153quests on its own project's events with an app token (audience `openvibe.quest`, capability `quest.app_quest.manage`).
154A step may count only `app.<project_key>.<name>` events of that project, and the person is the event's actor (an app
155publishing `on_behalf_of` a user; an app acting as itself is nobody). Only production counts: Events never hands
156sandbox 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
158for the app, never OpenCoins; at most 20 active quests a project; `/apps/quests/<id>` is the public page and the home
159page lists them under "Made by apps".
160
161A step can also name payload values to `match` (`{ task: 'emote' }`): the "Set up your channel" series is one
162quest 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
164coins 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
175Rewards are badges and OpenCoins. Nothing here is money, and nothing here is Vibes.
176
177## Configuration
178
179See [.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
195npm install
196fnm exec --using=22 npm test # every test/*.test.js, on temp PGlite databases with a mock Network
197fnm exec --using=22 npm run dev # http://localhost:4980
198```
199
200Without `DATABASE_URL` development uses an embedded PGlite database in `data/pglite` (one process only). `npm run
201test:pg` runs the same suite through PostgreSQL and PgBouncer (see [.github/workflows/ci.yml](.github/workflows/ci.yml)).
202
203The 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,
205idempotent 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
210an in-process mock of OpenVibe.Network ([test/helpers/mocks.js](test/helpers/mocks.js)); no network or running site is
211needed. `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
261A person's account at OpenVibe.Network can be exported and deleted, and every service holding their rows answers its
262part (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
265subscriptions 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
277The delivery receipts (`quest_event_inbox`) are this service's own, not a person's rows, and are neither exported nor
278erased.
279
280Environment: `QUEST_EVENTS_SECRET` (comma-separated for rotation, 32+ characters each; unset makes the route answer
281503), `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
286Reporting 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
300Part 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