openvibers/openvibe.help AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Help |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Help is the help and support centre for every OpenVibe site: answers generated from the pinned |
| 6 | OpenVibe.Contracts manifests, search over those answers plus OpenVibe.Search's public network index, and a support |
| 7 | ticket that stays one conversation with staff. |
| 8 | |
| 9 | - **Domain:** `openvibe.help` · **Port:** 5020 · **Service id:** `help` · **Env prefix:** `HELP` |
| 10 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (embedded PGlite in development/tests), |
| 11 | Valkey 9 for shared per-caller counters. |
| 12 | - **Read first:** [README.md](README.md) and [STATUS.json](STATUS.json). |
| 13 | |
| 14 | **Network context:** identity, sessions and staff roles are OpenVibe.Network (OAuth client `help`, PKCE S256; staff is |
| 15 | the Network role `admin` or `global_mod`). The help text is OpenVibe.Contracts (read-only, quoted verbatim). The |
| 16 | "From across OpenVibe" half of `/search` calls OpenVibe.Search's public API. Account export/deletion arrives from |
| 17 | OpenVibe.Events at `/internal/events`. Help owns no text, account, search index or event broker of its own. |
| 18 | |
| 19 | ## Commands |
| 20 | |
| 21 | ```bash |
| 22 | npm ci # install (contracts/sdk/shared are pinned by tag) |
| 23 | npm run dev # http://localhost:5020, NODE_ENV=development |
| 24 | npm start # node server/index.js |
| 25 | npm test # every test/*.test.js on temp PGlite, mocked Network/Search, no internet |
| 26 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 27 | npm test -- tickets # only test files whose name contains the word |
| 28 | npm test -- --strict # a skipped test fails the run |
| 29 | node --check <file.js> # syntax check (no linter configured) |
| 30 | ``` |
| 31 | |
| 32 | `test/run.js` uses `openvibe-shared/test-runner`, one process per file. There is **no build step**; pages are |
| 33 | server-rendered and static assets are served from [public/](public/). |
| 34 | |
| 35 | ## Architecture at a Glance |
| 36 | |
| 37 | - **Entry:** [server/index.js](server/index.js) — listener, telemetry flush/retention and the Events subscriptions; |
| 38 | graceful stop is `createLifecycle()` in the same file. |
| 39 | - **App factory:** [server/app.js](server/app.js) — helmet/CSP, SSO, `/auth`, `/internal/events`, static/`/shared`, |
| 40 | `/api/v1`, discovery + Markdown twins, pages, 404/error handler. Tests call `createApp({ store, fetchImpl, ... })`. |
| 41 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production reads `/etc/openvibe/help.env`. |
| 42 | Only variable names appear in code/docs. |
| 43 | - **Catalog (no database):** [server/catalog.js](server/catalog.js) reads every product and service manifest from |
| 44 | `openvibe-contracts` at boot, merges a domain that is both, and generates one article per manifest FAQ entry. |
| 45 | Nothing is stored; restart to pick up a new Contracts pin. Pages are [server/http/pages.js](server/http/pages.js). |
| 46 | - **Search:** [server/search.js](server/search.js) is the OpenVibe.Search client for network results; |
| 47 | [server/search-index.js](server/search-index.js) publishes Help's own article pages to Search using |
| 48 | `openvibe-publishing/search-feed` and the outbox tables `event_outbox` / `help_index_revisions`. |
| 49 | - **Tickets:** [server/tickets/](server/tickets/) (store, pages/routes, staff) — the only personal data Help owns. |
| 50 | `server/http/api.js` mounts the ticket API; [server/http/principal.js](server/http/principal.js) keeps |
| 51 | `CAPABILITIES` deliberately empty. |
| 52 | - **Auth/account data:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js) and |
| 53 | [server/identity/account-data.js](server/identity/account-data.js). |
| 54 | - **Database:** [server/db.js](server/db.js) — [migrations/](migrations/) at boot with `DATABASE_DIRECT_URL` (owner); |
| 55 | `DATABASE_URL` serves. Tables: `help_tickets`, `help_messages`, `account_data_events`, `event_outbox`, |
| 56 | `help_index_revisions`, `telemetry_samples` (plus the Contracts-free `things` fixture table). |
| 57 | - **Frontend:** server-rendered HTML through [server/render/layout.js](server/render/layout.js) and |
| 58 | [server/render/html.js](server/render/html.js), with the OpenVibe Frame/kit from `openvibe-shared`; assets in |
| 59 | [public/css/](public/css/). Pages work with JavaScript off. Markdown twins: |
| 60 | [server/agent-pages.js](server/agent-pages.js), [server/agent-descriptor.js](server/agent-descriptor.js). |
| 61 | - **Deploy:** [deploy/systemd/openvibe-help.service](deploy/systemd/openvibe-help.service) (127.0.0.1:5020, |
| 62 | `/etc/openvibe/help.env`) and [deploy/nginx/openvibe.help.conf](deploy/nginx/openvibe.help.conf). `sudo ovhost |
| 63 | deploy help`; database `ov_help`; checkout at `/opt/openvibe.help`; rollback restores the previous sha when |
| 64 | `/api/ready` fails after the restart. |
| 65 | |
| 66 | ## Ownership and Boundaries |
| 67 | |
| 68 | Help owns support tickets, their messages and the receipts of account-export/deletion events it applied. It does |
| 69 | **not** own accounts, roles or sessions (Network), the help text or manifests (Contracts), the network search index |
| 70 | (Search), event delivery (Events), or the host/database/Valkey (Host). The catalog is derived state: never put it in |
| 71 | a migration or cache it across a Contracts pin. A ticket is private: anyone but its requester gets 404, staff may read |
| 72 | all via their Network role. |
| 73 | |
| 74 | ## Auth and Capabilities |
| 75 | |
| 76 | - A person is the `help_at` session cookie or a Network user token verified offline with the Network JWKS. A |
| 77 | cookie-authenticated write must be same-origin (`request.cross_site`). |
| 78 | - Staff = Network role `admin` or `global_mod`; no `help.*` capability exists yet. `CAPABILITIES` in |
| 79 | [server/http/principal.js](server/http/principal.js) is empty on purpose — add a capability here and in |
| 80 | `openvibe-contracts` together when a route is for an app, never one alone. |
| 81 | - Outbound: `events.subscription.manage` at boot; `network.account.export.contribute` and |
| 82 | `network.account.deletion.confirm` through `openvibe-sdk/account-data`; `search.document.write` for the search feed. |
| 83 | - Problem codes are RFC 9457 `application/problem+json`: `token.required`, `help.person_required`, |
| 84 | `request.cross_site`, `staff.forbidden`, `ticket.not_found`, `help.site.not_found`, `ticket.invalid`, |
| 85 | `rate_limited`. |
| 86 | |
| 87 | ## API and Pages |
| 88 | |
| 89 | Pages: `/`, `/sites`, `/sites/:id`, `/a/:site/:slug`, `/search`, `/tickets`, `/tickets/:id`, `/staff`, `/updates`, |
| 90 | plus discovery (`/llms.txt`, `/sitemap.xml`, `/robots.txt`, `/.well-known/agent.json`) and the Markdown twins. |
| 91 | |
| 92 | `/api/v1`: `GET /ping`, `GET /sites`, `GET /sites/:id`, `GET /articles?q=`, `POST /tickets`, `GET /tickets`, |
| 93 | `GET /tickets/:id`, `POST /tickets/:id/messages`, `GET /staff/tickets` (staff), `POST /tickets/:id/state` (staff). |
| 94 | Machine endpoints `/api/health`, `/api/ready`, `/release.json`, `/metrics` are loopback only. |
| 95 | |
| 96 | ## Conventions |
| 97 | |
| 98 | - CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. |
| 99 | - Route handlers go through [server/http/router.js](server/http/router.js); errors from async handlers must reach the |
| 100 | error handler. Never log request bodies or ticket bodies. |
| 101 | - IDs are `tkt_`/`msg_` + ULID; reads are explicit field maps, not raw rows. |
| 102 | - New schema = a new `migrations/NNNN_name.sql`; never edit a migration that has run. |
| 103 | - CSP: every external origin belongs in the `helmet` directives in [server/app.js](server/app.js); `frameAncestors` |
| 104 | stays `'none'`. The kit/Frame come from the named OpenVibe hosts, not arbitrary CDNs. |
| 105 | - Phone first: every answer and ticket page must fit 390 px (and the longest at 320 px) with no horizontal scroll; |
| 106 | [test/phone-clipping.test.js](test/phone-clipping.test.js) measures this in Chrome. |
| 107 | - Empty states use the shared kit (`class="ovk-empty"` with a next action), not bespoke markup |
| 108 | ([test/empty-states.test.js](test/empty-states.test.js)). |
| 109 | - "Calm reduces, only Off removes": preferences may de-emphasize, never delete. Never add a hidden removal path. |
| 110 | - No model call belongs in Help: articles are manifest text, not generated copy. |
| 111 | |
| 112 | ## Key Pitfalls |
| 113 | |
| 114 | - **Catalog is rebuilt at boot.** A new site or FAQ is a Contracts change and a pin bump, not an edit here; a stale |
| 115 | pin means a missing site. Never hand-write an article. |
| 116 | - **Ticket privacy.** `GET /tickets/:id` returns 404 to anyone but the requester (staff excepted); keep that check on |
| 117 | every new read path. `page_url` is stored as text and never fetched. |
| 118 | - **No build step.** Editing [public/](public/) takes effect on deploy; content hashing adds `?v=`, so never hand-add |
| 119 | it. `home-short` and `perf-budget` tests fail when first paint or the home's size grows. |
| 120 | - **Account deletion** is loopback-only, signature-verified and idempotent: a redelivered `/internal/events` event |
| 121 | must change nothing. |
| 122 | - **Drill mode** (`HELP_DRILL`) keeps boot-time timers, outbound calls and subscriptions off; add any new one to it. |
| 123 | - **Markdown twins** are served before page routes so `/a/site/x.md` is a twin, not an article slug; new top-level |
| 124 | pages must be added to the page router and the sitemap/llms artifacts. |
| 125 | - **`event_outbox` indexing:** the rejected-count query needs the partial index in |
| 126 | [migrations/0006_outbox_rejected_index.sql](migrations/0006_outbox_rejected_index.sql); do not drop it. |
| 127 | - **No Font Awesome** in new UI; use the shared kit's glyphs. |
| 128 | |
| 129 | ## Testing |
| 130 | |
| 131 | `npm test` runs each `test/*.test.js` in its own process on a temp PGlite database with a mock Network. |
| 132 | [test/tickets.test.js](test/tickets.test.js), [test/help-articles.test.js](test/help-articles.test.js), |
| 133 | [test/help-search.test.js](test/help-search.test.js), [test/search-index.test.js](test/search-index.test.js), |
| 134 | [test/account-data.test.js](test/account-data.test.js), SSO/limits/secret-leak/redirect regressions, layout and |
| 135 | discovery. [test/phone-clipping.test.js](test/phone-clipping.test.js) uses a real Chrome and reports a skip when |
| 136 | Chrome is absent. Run the narrowest file (`npm test -- tickets`), then the suite; `npm run test:pg` for the PostgreSQL |
| 137 | path. Always `node --check` changed files. |
| 138 | |
| 139 | ## Documentation |
| 140 | |
| 141 | - [README.md](README.md) — purpose, ownership, capabilities, what it does, pages, API, configuration, tests, deploy, |
| 142 | account data, threat notes. |
| 143 | - [STATUS.json](STATUS.json) — stage, deployed state, features and not-yet list. |
| 144 | - [.env.example](.env.example) — every variable name (never commit values). |
| 145 | - [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md) — releases and vulnerability reporting. |
| 146 |