openvibers/openvibe.directory AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Directory |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | Source-linked directories for the OpenVibe network: OpenVibe sites, tools, AI models and providers, |
| 6 | open-source projects, local businesses from OpenStreetMap, and opt-in people listings. Every entry |
| 7 | carries a source and a last-checked date. Node.js/Express, server-rendered pages, one small |
| 8 | PostgreSQL table for people listings. |
| 9 | |
| 10 | - Origin `https://openvibe.directory`, port `5281`, unit `openvibe-directory.service`, |
| 11 | release root `/opt/openvibe.directory`. |
| 12 | - Public sources refresh at startup and every six hours; a failed source keeps its last successful |
| 13 | in-memory snapshot and the failure is visible in `/api/v1/kinds`. |
| 14 | |
| 15 | ## What it owns |
| 16 | |
| 17 | - `GET /`, `/kinds/:kind`, `/search`, `/entries/:kind/:slug` — HTML, with `Accept: text/markdown` on |
| 18 | entry pages. |
| 19 | - `GET /api/v1/kinds`, `/api/v1/entries`, `/api/v1/entries/:kind/:slug` — the public read API. |
| 20 | - `/api/v1/people/me` and `/people/me` — a signed-in person's own listing (get, put, delete). |
| 21 | - `robots.txt`, `sitemap.xml`, `llms.txt`, JSON-LD, `/release.json`, `/shared/*`, `/api/health`, |
| 22 | `/api/ready`. |
| 23 | - The `directory_people` table and its validation. |
| 24 | |
| 25 | It does not own the sources: it reads Network's frame, Tools, Exchange models, GitHub's public search |
| 26 | and OpenStreetMap. It does not publish anything outside this site. |
| 27 | |
| 28 | ## Where the data comes from |
| 29 | |
| 30 | [server/sources.js](server/sources.js) `URLS`/`kinds`: |
| 31 | |
| 32 | - `https://openvibe.network/api/frame?host=openvibe.network` — OpenVibe sites. |
| 33 | - `https://openvibe.tools/api/v1/tools` — tools. |
| 34 | - `https://openvibe.exchange/api/v1/models` — AI models and providers. |
| 35 | - `https://api.github.com/search/repositories?q=org%3AOpenVibers+archived%3Afalse` — projects. |
| 36 | - `https://api.openstreetmap.org/api/0.6/map?bbox=…` — a **bounded San Francisco pilot** under |
| 37 | ODbL 1.0. |
| 38 | - People come from PostgreSQL, not a feed. |
| 39 | |
| 40 | Every entry is normalised (`entry()`, `slug()`, `safeUrl()`): a missing id/name/source or a non-https |
| 41 | source is dropped rather than guessed. |
| 42 | |
| 43 | ## Repository map |
| 44 | |
| 45 | - **Entry:** [server/index.js](server/index.js) — builds the source store, opens the people store when |
| 46 | `DATABASE_URL` is set, listens on `PORT || 5281`. |
| 47 | - **App/routes:** [server/app.js](server/app.js) — security headers, static `/public`, `/shared`, |
| 48 | `/auth`, health/ready, release, the v1 API, pages, people form, 404. |
| 49 | - **Sources:** [server/sources.js](server/sources.js) — fetch/refresh, normalisation, search. |
| 50 | - **People:** [server/people.js](server/people.js) — `createPeopleStore` (migration + CRUD), `validate`, |
| 51 | `publicEntry`, `writeGuard`, `createAuth`. |
| 52 | - **Views:** [server/views.js](server/views.js) — HTML/markdown, escaping, JSON-LD. |
| 53 | - **Browser:** [public/people.js](public/people.js). |
| 54 | - **Schema:** [migrations/0001_people.sql](migrations/0001_people.sql). |
| 55 | - **Deploy:** [deploy/systemd/openvibe-directory.service](deploy/systemd/openvibe-directory.service), |
| 56 | [deploy/nginx/openvibe.directory.conf](deploy/nginx/openvibe.directory.conf). |
| 57 | |
| 58 | ## Commands |
| 59 | |
| 60 | ```bash |
| 61 | npm ci |
| 62 | node server/index.js # http://127.0.0.1:5281; start script is `npm start` |
| 63 | npm test # node --test test/*.test.js |
| 64 | ov test # the harness's sharded run of what the change reaches |
| 65 | node --check <file.js> |
| 66 | ``` |
| 67 | |
| 68 | There is no `dev` script and no build step. Without `DATABASE_URL` the directories still serve from |
| 69 | the feeds; the people routes answer `503 people.unavailable`. |
| 70 | |
| 71 | ## Database and migrations |
| 72 | |
| 73 | One table, `directory_people`, created from [migrations/0001_people.sql](migrations/0001_people.sql) |
| 74 | **at startup** by [server/people.js](server/people.js), not by a boot migration runner: the file is |
| 75 | read and applied through the **owner** connection (`DATABASE_DIRECT_URL`), while reads and writes use |
| 76 | the runtime connection (`DATABASE_URL`). The statement is `CREATE TABLE IF NOT EXISTS`, so startup is |
| 77 | idempotent. Keep that split: the test asserts exactly one owner call (the DDL) and then a runtime |
| 78 | `SELECT`. Listing text is capped at 500 characters, names at 100, websites must be `https:` with no |
| 79 | credentials, and `visible` defaults to **false**. |
| 80 | |
| 81 | ## Auth and the people listing |
| 82 | |
| 83 | - Sign-in is OpenVibe.Network SSO via `openvibe-sdk/sso` ([server/people.js](server/people.js) |
| 84 | `createAuth`); without `OV_OAUTH_CLIENT_ID`/`SECRET` the auth object is absent and the people routes |
| 85 | are disabled. |
| 86 | - Only the person's own row: the subject comes from the session (`usr_…`); never take an id from the |
| 87 | query or body. |
| 88 | - `writeGuard` requires the same-origin `Origin` (or no Origin plus a Bearer token) and |
| 89 | `application/json` (DELETE excepted); invalid requests answer 403/415. |
| 90 | - An entry is private until the person explicitly publishes it (`visible: true`). |
| 91 | |
| 92 | ## Frontend loading model |
| 93 | |
| 94 | Server-rendered HTML from [server/views.js](server/views.js) with `openvibe-shared/kit`, |
| 95 | `/thread`, `/shell` and `/serve`; **no SPA, no bundler**. Static files come from `public/`; |
| 96 | `/shared/*` is served from the pinned `openvibe-shared` node_modules by `openvibe-shared/serve`. |
| 97 | The only page script is [public/people.js](public/people.js). The home page redirects a signed-out |
| 98 | browser to silent SSO unless `?sso=none` or the guest hint cookie is set — keep that for crawlers |
| 99 | (`bot|crawl|spider|slurp` must not be redirected). |
| 100 | |
| 101 | ## Deploy |
| 102 | |
| 103 | Unit [deploy/systemd/openvibe-directory.service](deploy/systemd/openvibe-directory.service) runs |
| 104 | `/opt/openvibe.directory/server/index.js` on `127.0.0.1:5281`, env `/etc/openvibe/directory.env`; |
| 105 | vhost [deploy/nginx/openvibe.directory.conf](deploy/nginx/openvibe.directory.conf). The repository does |
| 106 | not document an `ovhost deploy` command — change the unit/vhost with the host's own installer rather |
| 107 | than inventing one. No secret belongs in a file or a log. |
| 108 | |
| 109 | ## Tests |
| 110 | |
| 111 | `npm test` runs Node's built-in runner over [test/directory.test.js](test/directory.test.js) and |
| 112 | [test/people.test.js](test/people.test.js). They pin: OSM phone-tag omission and no age/city/phone |
| 113 | fields; unlisted by default; the owner-connection DDL / runtime-read split; entry validation; the |
| 114 | public pages, markdown negotiation, sitemap and llms output. `ov test` runs the same suite. |
| 115 | |
| 116 | ## Conventions |
| 117 | |
| 118 | - CommonJS; **compact single-line JS** is the style in `server/app.js`, `server/sources.js` and |
| 119 | `server/people.js`; the newer view helpers use 4 spaces. Match the file you edit. |
| 120 | - Escape every interpolated value (`views.esc`); embed JSON with the `<`-escaping `json()`. |
| 121 | - Errors are small JSON bodies (`{ error: 'entry.not_found' }`) or problem-style statuses; do not leak |
| 122 | upstream text. |
| 123 | - Source URLs are `https:` only; never invent a source, a checked date or a phone number. |
| 124 | |
| 125 | ## Pitfalls |
| 126 | |
| 127 | - **Unverified OSM phone tags are omitted.** A business number needs a business-owned source before |
| 128 | publication; do not re-add phone fields. |
| 129 | - **No private-person searches or reverse lookups.** People listings are claim/edit/unpublish/delete |
| 130 | of one's own row only. |
| 131 | - **The SF business pilot is bounded and ODbL 1.0.** Do not widen the bounding box or change the |
| 132 | licence without the owner. |
| 133 | - **A failed source keeps its last snapshot** and reports the failure; it never empties the directory |
| 134 | or silently serves stale data as fresh. |
| 135 | - **The startup migration uses the owner connection**; using `DATABASE_URL` for DDL will fail in |
| 136 | production and breaks the test. |
| 137 | - **`visible` defaults false.** Publishing is an explicit act by the person. |
| 138 | - **Crawler behavior:** home may redirect signed-out browsers to silent SSO; bots must not be |
| 139 | redirected and must still see the page. |
| 140 | |
| 141 | ## Documentation |
| 142 | |
| 143 | - [README.md](README.md) — the product brief, sources, licence and the database split. |
| 144 | - [migrations/0001_people.sql](migrations/0001_people.sql) — the only table. |
| 145 | - OpenVibe.Contracts, if a directory manifest is added, is the place for the public schema. |
| 146 |