openvibers/openvibe.fun AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Fun |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Fun is the Fun room: IRL quest boards attached to a public OpenVibe.Live channel, a deterministic |
| 6 | "what's fun near me" guide from OpenStreetMap (Ticketmaster only when a key is configured), and group plans with one |
| 7 | vote each and a winner the viewer can add to their own OpenVibe.Today. |
| 8 | |
| 9 | - **Domain:** `openvibe.fun` · **Port:** 5140 · **Service id:** `fun` · **Env prefix:** `FUN` |
| 10 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (embedded PGlite in development/tests), |
| 11 | Valkey 9 for shared limit counters. |
| 12 | - **Read first:** [README.md](README.md) (ownership, API and safety rules) and [STATUS.json](STATUS.json). |
| 13 | |
| 14 | **Network context:** identity, roles and the OpenCoins wallet are OpenVibe.Network (OAuth client `fun`, PKCE S256; |
| 15 | staff means Network role `global_mod` or above). Fun **reads** Live's public channel state and Chat's public history |
| 16 | through `server/upstreams.js`; it never posts to Live or Chat. Today is written only with the signed-in person's own |
| 17 | Network token (`server/today.js`). Account export/deletion arrives from OpenVibe.Events at `/internal/events`. Search |
| 18 | documents wait in `fun_outbox` and are delivered by `openvibe-publishing/search-relay` (`server/search.js`). Fun owns |
| 19 | no other service's state. |
| 20 | |
| 21 | ## Commands |
| 22 | |
| 23 | ```bash |
| 24 | npm ci # install (the repo pins contracts/sdk/shared by tag) |
| 25 | npm run dev # http://localhost:5140, NODE_ENV=development |
| 26 | npm start # node server/index.js |
| 27 | npm test # every test/*.test.js in its own process, temp PGlite, mocked Network/upstreams |
| 28 | npm run test:pg # the same through PostgreSQL + PgBouncer (openvibe-sdk scripts/test-services.sh up) |
| 29 | npm test -- quests # only test files whose name contains the word |
| 30 | npm test -- --strict # a skipped test fails the run |
| 31 | node --check <file.js> # syntax check (no linter/formatter in this repo) |
| 32 | ``` |
| 33 | |
| 34 | `test/run.js` uses `openvibe-shared/test-runner`; tests never reach the internet — upstream fixtures are local. There |
| 35 | is **no build step**: pages are server-rendered and assets are served from `public/` with content-hash cache headers. |
| 36 | |
| 37 | ## Architecture at a Glance |
| 38 | |
| 39 | - **Entry:** [server/index.js](server/index.js) — starts the listener, telemetry tick/retention, the read-only chat |
| 40 | sync, the OpenCoin retry timer and the Events subscriptions; `FUN_DRILL` keeps boot-time work off. Graceful stop is |
| 41 | `createLifecycle()` in the same file. |
| 42 | - **App factory:** [server/app.js](server/app.js) — helmet/CSP, SSO middleware, `/auth`, `/internal/events`, |
| 43 | `/shared` + static, `/api/v1`, the agent layer (Markdown twins and `/.well-known/agent.json`), pages, 404 and error |
| 44 | handler. Tests build this app with `createApp({ store, fetchImpl, ... })`. |
| 45 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production reads `/etc/openvibe/fun.env`. |
| 46 | Only variable **names** appear in code and docs. |
| 47 | - **Database:** [server/db.js](server/db.js) — migrations in [migrations/](migrations/) run at boot with |
| 48 | `DATABASE_DIRECT_URL` (owner); `DATABASE_URL` serves through PgBouncer. No table is created at runtime. |
| 49 | - **Store/domain:** [server/store.js](server/store.js), [server/quests/](server/quests/) (`ideas.js`, `safety.js`), |
| 50 | [server/guide/](server/guide/) (`index.js`, `places.js`, `upstream.js`, `events.js`), |
| 51 | [server/chat-sync.js](server/chat-sync.js), [server/coins.js](server/coins.js), |
| 52 | [server/sharing.js](server/sharing.js), [server/today.js](server/today.js). |
| 53 | - **HTTP:** [server/http/router.js](server/http/router.js) is the async router; [server/http/api.js](server/http/api.js) |
| 54 | mounts `/api/v1`, [server/http/pages.js](server/http/pages.js) the pages, [server/http/principal.js](server/http/principal.js) |
| 55 | decides person vs service token and capabilities. Views are under [server/views/](server/views/). |
| 56 | - **Auth:** [server/auth/sso.js](server/auth/sso.js) + [server/auth/keys.js](server/auth/keys.js) — Network OAuth2 |
| 57 | PKCE and offline JWKS verification; staff from the Network role claim ([server/identity/staff.js](server/identity/staff.js)). |
| 58 | Account export/deletion: [server/identity/account-data.js](server/identity/account-data.js). |
| 59 | - **Frontend:** server-rendered HTML through [server/render/layout.js](server/render/layout.js) and |
| 60 | [server/render/html.js](server/render/html.js), which pull the OpenVibe Frame/kit from `openvibe-shared`. Assets: |
| 61 | [public/css/fun.css](public/css/fun.css), [public/js/fun.js](public/js/fun.js). Pages work without JavaScript; JS |
| 62 | only adds convenience. Markdown twins come from [server/agent-pages.js](server/agent-pages.js) and |
| 63 | [server/agent-descriptor.js](server/agent-descriptor.js). |
| 64 | - **Deploy:** [deploy/systemd/openvibe-fun.service](deploy/systemd/openvibe-fun.service) (127.0.0.1:5140, |
| 65 | `/etc/openvibe/fun.env`) and [deploy/nginx/openvibe.fun.conf](deploy/nginx/openvibe.fun.conf) (TLS, www → apex, |
| 66 | `/internal` and `/metrics` closed). `sudo ovhost deploy fun` on the host; database `ov_fun`; /opt/openvibe.fun. |
| 67 | |
| 68 | ## Ownership and Boundaries |
| 69 | |
| 70 | Fun is the authority for the tables its migrations create: quest boards/quests/votes/checks/badges/rewards, plans and |
| 71 | their options/votes, `fun_reports`, `fun_chat_seen`/`fun_chat_cursors`, `fun_geo_cache`, `fun_outbox`, |
| 72 | `account_data_events`, sharing tables and `telemetry_samples`. It is **not** the authority for: |
| 73 | |
| 74 | - identity, accounts, roles, sessions or the wallet — OpenVibe.Network; |
| 75 | - Live channels, Chat rooms/history, Today calendars — read-only upstreams or the person's own token; |
| 76 | - OSM/Ticketmaster data — cached, attributed public sources, never paid providers; |
| 77 | - event delivery (OpenVibe.Events), the host/database/Valkey (OpenVibe.Host), or the search index (OpenVibe.Search). |
| 78 | |
| 79 | Public quests/plans are anonymized on account deletion; shared plans stay for their other members. Never silently keep |
| 80 | a deleted person's name on a row. |
| 81 | |
| 82 | ## Auth and Capabilities |
| 83 | |
| 84 | - A person is the `fun_at` session cookie or `Authorization: Bearer <Network user token>`, verified offline. A |
| 85 | cookie-authenticated write must come from `https://openvibe.fun` (403 `fun.cross_site`). |
| 86 | - Per-actor limits: `FUN_LIMITS_MINUTE`, `FUN_LIMITS_HOUR`; shared counters use Valkey. |
| 87 | - **Inbound v1: no service capability guards a route.** Do not invent one. |
| 88 | - **Outbound:** `events.subscription.manage` at boot; `network.account.export.contribute` and |
| 89 | `network.account.deletion.confirm` through `openvibe-sdk/account-data`; `search.document.write` for the Search |
| 90 | relay; `network.coins.credit` + `identity.subject.resolve` only when `FUN_COINS=on`. |
| 91 | - The OpenCoin credit is an outbox (`fun_quest_rewards`) with retries; the local badge is always paid. |
| 92 | |
| 93 | ## API (JSON under `/api/v1`) |
| 94 | |
| 95 | `GET /guide`, `GET /quests`, `GET /quests/ideas`, `GET /quests/boards/:id`, `GET /quests/:slug`, |
| 96 | `POST /quests/boards`, `POST /quests/boards/:id/suggest|idea`, `POST /quests/:slug/vote|accept|live|complete|verify|hide|report`; |
| 97 | `GET /plans`, `GET /plans/:slug`, `POST /plans`, `POST /plans/:slug/options|vote|decide|today|hide|report`; |
| 98 | `GET /reports` (staff). Discovery/health: `/api/v1/ping`, `/openapi.json`, `/llms.txt`, `/sitemap.xml`, `/robots.txt`, |
| 99 | `/api/health`, `/api/ready`, `/release.json`, `/metrics` (loopback only). Errors are RFC 9457 |
| 100 | `application/problem+json` with a stable `code` (`fun.invalid`, `fun.quest.*`, `fun.plan.*`, `fun.rate_limited`, …). |
| 101 | |
| 102 | ## Conventions |
| 103 | |
| 104 | - CommonJS (`require`/`module.exports`), 4-space indent, single quotes, semicolons; no linter/formatter. |
| 105 | - Every route handler goes through [server/http/router.js](server/http/router.js) so async rejections reach the error |
| 106 | handler. Never log request bodies; never return raw rows — page/view code renders explicit fields. |
| 107 | - IDs are `prefix_` + ULID via the store; timestamps are ISO strings from the injected clock. |
| 108 | - New schema = a new `migrations/NNNN_name.sql`; never edit a migration that has run. Keep table prefixes `fun_`. |
| 109 | - CSP: add every external origin to the `helmet` directives in [server/app.js](server/app.js); images/frames default |
| 110 | to self plus the named OpenVibe hosts. Do not widen `frameAncestors` or `objectSrc`. |
| 111 | - Phone first: pages must work at 390 px with no horizontal overflow; the navbar/footer come from `openvibe-shared`. |
| 112 | - "Calm reduces, only Off removes": a user preference may de-emphasize content, never delete it; an Off switch is the |
| 113 | only removal. |
| 114 | - Public pages must have real content in the first paint — no placeholder text or fake counts (`home-real-content`, |
| 115 | `home-real-first` tests); link-outs must be real (`remind-links`). |
| 116 | - Do not add model calls or a new AI capability here: v1 is deterministic. |
| 117 | |
| 118 | ## Key Pitfalls |
| 119 | |
| 120 | - **No build step.** Editing `public/` is live on deploy; do not add `?v=` by hand — the layout's asset version does |
| 121 | that. |
| 122 | - **Upstreams are public and optional.** Ticketmaster is off without `TICKETMASTER_API_KEY`; OSM must be paced, |
| 123 | cached, attributed (ODbL) and has a stale fallback. Never fetch an arbitrary user URL. |
| 124 | - **Read-only chat.** `server/chat-sync.js` ingests `!quest`/`!plan` commands idempotently per chat message id; it |
| 125 | must never write to Chat or Live. |
| 126 | - **Drill mode.** Anything started at boot (timers, loops, outbound calls) must be off under `FUN_DRILL`. |
| 127 | - **Agent layer.** `.md` paths and `?format=markdown` are handled before page routes so `/q/x.md` is a twin, not a |
| 128 | quest slug. New top-level page routes must also be visible to [server/http/pages.js](server/http/pages.js) and the |
| 129 | sitemap/llms artifacts. |
| 130 | - **Safety filter.** `server/quests/safety.js` is explainable and must keep rejecting dangerous, illegal, |
| 131 | trespassing and harassing suggestions before storage. |
| 132 | - **Account deletion** runs through `/internal/events` only (loopback, signature-verified); a redelivered event must |
| 133 | change nothing. |
| 134 | - **No Font Awesome** in new UI unless the shared kit/glyph test for the page allows it; use the shared glyph set. |
| 135 | |
| 136 | ## Testing |
| 137 | |
| 138 | `npm test` boots every `test/*.test.js` in its own process on a temporary PGlite database with a mock Network and |
| 139 | fixture upstreams (`test/run.js`). The suite covers migration, quest safety/state machine, chat idempotency, plan |
| 140 | voting/tie/Today, Search documents, SSO, cache/pages, coins opt-in off/on, sharing, account export/deletion, agent |
| 141 | layer, real-content-first and broken-link regressions. `npm run test:pg` runs the same suite through PostgreSQL + |
| 142 | PgBouncer. Run the narrowest file first (`npm test -- quests`), then the suite. Always `node --check` changed files. |
| 143 | |
| 144 | ## Documentation |
| 145 | |
| 146 | - [README.md](README.md) — purpose, owns/does-not-own, capabilities, API, quest safety, guide, plans, pages, Search, |
| 147 | run/test, deploy. |
| 148 | - [STATUS.json](STATUS.json) — current stage, deployed flag, features and not-yet list. |
| 149 | - [.env.example](.env.example) — every variable (names only; never commit values). |
| 150 | - [CHANGELOG.md](CHANGELOG.md) — released changes. |
| 151 | - [SECURITY.md](SECURITY.md) — vulnerability reporting. |
| 152 |