openvibers/openvibe.community AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Community |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | The OpenVibe community home: **pastes** (the only authority for them since 2026-09-22), the **forum** |
| 6 | (spaces, threads, posts, votes), the **typed comment threads** every other product embeds, **Pulse** |
| 7 | (the network's public activity), **submissions**, the Discord relay, and members-only gates through |
| 8 | VIP. A small Node.js/Express app, CommonJS, no frontend framework, one PostgreSQL database |
| 9 | (`ov_community`), server-rendered pages with a little progressive JavaScript. |
| 10 | |
| 11 | - Origin `https://openvibe.community`, port `4200`, unit `openvibe-community.service`, release root |
| 12 | `/opt/openvibe.community`. |
| 13 | - The forum moved back here from Space on 2026-10-08; Space redirects its old forum URLs here. |
| 14 | Space remains the code-hosting / "spaces" product. |
| 15 | |
| 16 | ## What it owns |
| 17 | |
| 18 | - Pastes, their versions, likes, visibility (public/unlisted/private/burn), locked pastes, spam and |
| 19 | image review, and the `/api/pastes/*` native API. Pages, raw text, screenshots and downloads read |
| 20 | from the paste store — never from Media (Media redirects its old paste URLs here). |
| 21 | - The typed comment threads every product embeds (`comments` with a type + ref, not a copy of the |
| 22 | product's content; the product stores only the thread id). |
| 23 | - The forum: spaces, groups, threads, posts, votes, categories, pinned starters, the Roadmap space |
| 24 | (`docs/roadmap/public.json`), a space's Chat room, members-only spaces/threads (VIP). |
| 25 | - Pulse, the network's public activity; submissions and their review queue; the Discord relay. |
| 26 | - `community.*` events, Search documents for public threads, the `community.profile` user module. |
| 27 | - `ov_community` (host data role, ADR-035), Valkey for per-actor limits. |
| 28 | |
| 29 | Not owned: identity/SSO (Network), file bytes (Media), memberships (VIP), chat rooms (Chat), the |
| 30 | contents of products whose threads it hosts. |
| 31 | |
| 32 | ## Who calls it, and whom it calls |
| 33 | |
| 34 | - Callers: people in the browser; Live (old VOD/clip comment imports), every product embedding a |
| 35 | comment thread, Chat/Space linking a forum thread, and services with a Network service token |
| 36 | audience `openvibe.community` and the route's capability. |
| 37 | - Depends on: **Network** (SSO, JWKS, service tokens, `identity.subject.resolve`, the profile module); |
| 38 | **Media** (screenshots/attachments; metadata stripped before upload); **VIP** (members-only gates, |
| 39 | fails closed); **Chat** (a space's room); **Events** (outbox and the Pulse/account subscriptions); |
| 40 | **Vision** (`OV_VISION_URL`, unsafe-image review; unset means no image review); Search for public |
| 41 | thread documents; `openvibe-contracts`, `openvibe-sdk` and `openvibe-shared` pins. |
| 42 | |
| 43 | ## Repository map |
| 44 | |
| 45 | - **Entry:** [server/index.js](server/index.js) — database/migrations first, app, Discord relay, Events, |
| 46 | Roadmap and starter sync; graceful stop. |
| 47 | - **App:** [server/app.js](server/app.js) — helmet/CSP, `/auth`, `/api/pastes`, `/api/v1/*` (comments, |
| 48 | spaces, posts, pulse, feed, submissions, profiles, relay, resources), `/shared`, `/internal/events`, |
| 49 | server-rendered pages. |
| 50 | - **Config/db:** [server/config.js](server/config.js), [server/db.js](server/db.js). |
| 51 | - **Pastes:** [server/pastes/](server/pastes/) — `api.js`, `service.js`, `store.js`, `catalog.js`, |
| 52 | `review.js`, `content-hash.js`, `spam.js`. |
| 53 | - **Comments:** [server/comments/](server/comments/) — `service.js`, `api.js`, `store.js`, `routes.js`. |
| 54 | - **Forum:** [server/forum/](server/forum/) — `service.js`, `api.js`, `routes.js`, `roadmap.js`, |
| 55 | `starters.js`. |
| 56 | - **Pulse/submissions/relay:** [server/pulse/](server/pulse/), [server/submissions/](server/submissions/), |
| 57 | [server/relay/](server/relay/). |
| 58 | - **Identity/auth:** [server/identity/](server/identity/), [server/auth/](server/auth/), |
| 59 | [server/vip/](server/vip/). |
| 60 | - **Render:** [server/render/](server/render/) — pages, comments, forum, feed, pulse, markdown, |
| 61 | highlight, JSON-LD. |
| 62 | - **Browser:** [public/js/community.js](public/js/community.js), `inline-comments.js`, `composer.js`; |
| 63 | CSS in [public/css/](public/css/); assets in [public/assets/](public/assets/). |
| 64 | - **Deploy:** [deploy/systemd/openvibe-community.service](deploy/systemd/openvibe-community.service), |
| 65 | [deploy/nginx/openvibe.community.conf](deploy/nginx/openvibe.community.conf). |
| 66 | |
| 67 | ## Commands |
| 68 | |
| 69 | ```bash |
| 70 | npm ci |
| 71 | npm run dev # NODE_ENV=development node server/index.js |
| 72 | npm start # node server/index.js, http://127.0.0.1:4200 |
| 73 | npm test # test/run.js, PGlite, mock Live/Network |
| 74 | npm run test:pg # same suite on PostgreSQL (COMMUNITY_TEST_STORE=pg) |
| 75 | npm test -- seo proxy # filter by file name; --strict makes a skip fail |
| 76 | npm run n-1:record # record the N-1 fixtures after a deploy |
| 77 | ov test # the harness's sharded run of what the change reaches |
| 78 | ``` |
| 79 | |
| 80 | ## Database and migrations |
| 81 | |
| 82 | `ov_community` on the host's data role (ADR-035); embedded PGlite in development/tests. Async through |
| 83 | `openvibe-sdk/db`. Migrations are `migrations/NNNN_*.sql`, applied at boot; the schema is created |
| 84 | idempotently in every mode (comments, forum, Pulse and the relay live here whichever service owns |
| 85 | pastes), and three seed spaces are inserted. Never edit a migration that has run; add a new number. |
| 86 | A rollback is safe because migrations only add tables and columns. |
| 87 | |
| 88 | ## Auth, identity and comments |
| 89 | |
| 90 | - People are **Network subjects** (`usr_…`, `gst_…`), never service-local integers. A browser's owner |
| 91 | is the JWT's `subject_id` (older tokens resolved once through Network and cached in `legacy_id_map`); |
| 92 | names/avatars come from `subject_projection`, refreshed through the Network's resolve-batch. |
| 93 | - Identity is **never read from bodies or queries**; a token issued before `token_valid_after` is |
| 94 | refused. `openvibe-sdk/sso` owns the cookie flow. |
| 95 | - Service callers need a service token audience `openvibe.community` and the route's capability |
| 96 | (`community.paste.create|write|moderate`, the comment/forum equivalents, …). An `X-OV-Subject` scopes |
| 97 | a first-party service call. |
| 98 | - Comments are typed: a product passes its own ref and gets a thread id; it never stores comment text. |
| 99 | - Members-only spaces/threads go through VIP and **fail closed**; locked pastes are sealed and never |
| 100 | readable from the text path ([docs/locked-pastes.md](docs/locked-pastes.md)). |
| 101 | |
| 102 | ## Frontend loading model |
| 103 | |
| 104 | Every page is server-rendered — crawlers and no-JS readers get the whole thing — with small no-build |
| 105 | scripts for pagination, copy buttons, the upload path and the inline composer. There is no SPA, |
| 106 | bundler or transpiler. `openvibe-shared` supplies the Frame, the share sheet and `/shared/*` from the |
| 107 | pinned node_modules (including `/shared/ov-glyphs.js`). **Icons are OpenVibe glyphs and brand logos, |
| 108 | never Font Awesome** (see the pitfall). Markdown is rendered through the local renderer, which escapes |
| 109 | first and only links http(s). |
| 110 | |
| 111 | ## Deploy |
| 112 | |
| 113 | `sudo ovhost deploy community` (git-checkout: fetch, fast-forward `/opt/openvibe.community`, install |
| 114 | on a lockfile change, restart, wait for `/api/ready`). Unit `openvibe-community.service` on |
| 115 | `127.0.0.1:4200`, env `/etc/openvibe/community.env` (0600), vhost |
| 116 | [deploy/nginx/openvibe.community.conf](deploy/nginx/openvibe.community.conf). Database `ov_community` |
| 117 | on the data role (`add-service.sh community`). Record the N-1 fixtures after a deploy |
| 118 | (`npm run n-1:record`). Rollback `sudo ovhost rollback community --to <sha>`. |
| 119 | |
| 120 | ## Tests |
| 121 | |
| 122 | `npm test` runs each `test/*.test.js` in its own process through `openvibe-shared/test-runner` |
| 123 | (60 s/file) with in-process Live/Network mocks. Notable guards: the no-FA guard (no FA link, font or unknown |
| 124 | `fa-` name; every icon page loads the glyph drop-in), |
| 125 | `test/empty-states.test.js`, `test/markdown.test.js` (an unsafe |
| 126 | scheme never becomes a link), `test/members-only.test.js`, `test/privacy.test.js`, the Pulse source |
| 127 | rules, `test/nginx-auth-limit.test.js`, `test/nginx-internal-paths.test.js` and the N-1 fixtures. |
| 128 | `ov test` runs the same. |
| 129 | |
| 130 | ## Conventions |
| 131 | |
| 132 | - CommonJS, 4-space indent, single quotes, semicolons; domain logic in `server/pastes|comments|forum`, |
| 133 | rendering in `server/render/`, no SQL in routes. |
| 134 | - Escape every interpolated value (`esc`), embed JSON with the `<`-escaping helper; never build HTML |
| 135 | from unsanitised Markdown. |
| 136 | - Errors are small JSON/problem bodies with stable codes; never leak a private paste, a member-only |
| 137 | body or another person's data through an error or a cache header. |
| 138 | - Works with JavaScript off; a feature that requires a script must not be the only path. |
| 139 | |
| 140 | ## Pitfalls |
| 141 | |
| 142 | - **No Font Awesome.** The no-FA guard fails on a page/stylesheet that links it, or on |
| 143 | an `fa-` name no glyph/brand draws. Use `openvibe-shared/glyphs` and `openvibe-shared/brands`. |
| 144 | - **Community is the only paste authority.** Do not add a second store or read pastes from Media/Live. |
| 145 | - **Identity comes from the verified token only** — never a body or a query. |
| 146 | - **Locked pastes are sealed at rest**, readable only through their gate; a share row must not make an |
| 147 | unpublished/private paste readable. |
| 148 | - **Pulse excludes unlisted, private, burn, NSFW and AI pastes**; do not "fix" a missing item by |
| 149 | widening that filter. |
| 150 | - **Strip image metadata before upload**; a 1×1 PNG's tEXt chunk must not reach Media. |
| 151 | - **Members-only content fails closed** when VIP is unavailable: teaser, not body. |
| 152 | - **Phone first is a standing product rule.** There is no automated overflow test here: check a |
| 153 | 390 px viewport by hand (and the forum/commenter grids) before shipping a layout change. |
| 154 | - **Empty states are part of the product**; a blank page for no posts/pastes/comments is a bug. |
| 155 | - **`/internal/*` and `/metrics` stay off the public vhost** (nginx tests). |
| 156 | - **N-1 fixtures** are recorded after a deploy; keep them current. |
| 157 | |
| 158 | ## Documentation |
| 159 | |
| 160 | - [README.md](README.md) — every surface, the endpoint list, principles, acceptance and deploy. |
| 161 | - [docs/locked-pastes.md](docs/locked-pastes.md), [docs/sharing.md](docs/sharing.md), |
| 162 | [docs/discord-relay.md](docs/discord-relay.md). |
| 163 | - [docs/forum/](docs/forum/), [docs/roadmap/](docs/roadmap/), [docs/screenshots/](docs/screenshots/). |
| 164 | - `openvibe-contracts` holds `community.*` shapes/capabilities; VIP and Media own their own. |
| 165 |