openvibers/openvibe.wiki README.md
README.md
| 1 | # OpenVibe.Wiki |
| 2 | |
| 3 | > Wikis with page trees, revisions, citations, media and discussion — editable together, source-backed. |
| 4 | |
| 5 | **Live site:** https://openvibe.wiki · **API docs:** https://openvibe.services · **For AI:** https://openvibe.wiki/llms.txt |
| 6 | **Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test` |
| 7 | |
| 8 | **Status:** alpha (roadmap Wave 16, Wiki half). **Public at https://openvibe.wiki since 2026-09-23** |
| 9 | (the launch release also removed the domain from [OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites)). |
| 10 | The only content is the official space's 42 AI-assisted seed pages, one for each network repository; no person has reviewed them yet, |
| 11 | so every page is `noindex` and none is in Search. |
| 12 | **Domain:** `openvibe.wiki` · **Port:** 4800 · **Service id:** `wiki` |
| 13 | **Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 — roadmap Wave 16, §15.13, §29, §32. |
| 14 | **License:** AGPL-3.0 (same as every OpenVibe service). |
| 15 | |
| 16 | ## Purpose |
| 17 | |
| 18 | The knowledge product of the network: wikis (official editorial wikis and community ones, called spaces in the code and API), page |
| 19 | trees with canonical slugs, immutable revisions with diff and revert, manual editing by permitted |
| 20 | people, typed infoboxes, `[[internal links]]`, citations attached to the revision that used them, |
| 21 | Media attachments and Community discussion. AI proposes revisions; a person approves them; Wiki |
| 22 | publishes. Search indexes only published, public state. |
| 23 | |
| 24 | It is the first full consumer of the shared publishing packages |
| 25 | ([openvibe-publishing](https://github.com/OpenVibers/OpenVibe.Publishing) v1.7.0, ADR-019): revisions, |
| 26 | citations, redirects, the indexability gate, feeds, sitemaps, JSON-LD, authorship, scheduling, |
| 27 | media references, discussion references and the Search index hooks all come from there. Wiki owns |
| 28 | its publication state; the packages supply the mechanics. |
| 29 | |
| 30 | Sitemap entries carry lastmod from each published page or space's latest change. |
| 31 | |
| 32 | ## Owns |
| 33 | |
| 34 | The ten authority tables of §15.13, in Wiki's own PostgreSQL database (`ov_wiki` on the host's data role, ADR-035; schema in [migrations/](migrations/)): |
| 35 | |
| 36 | | Table | What | Mechanics | |
| 37 | |---|---|---| |
| 38 | | `wiki_spaces` | spaces: official / user, public / members / private | Wiki | |
| 39 | | `wiki_pages` | canonical page identity, slug, tree (`parent_id`), publication state, published revision | Wiki | |
| 40 | | `wiki_page_revisions` | immutable revisions (UPDATE/DELETE refused by PL/pgSQL triggers) | `openvibe-publishing/revisions`, prefix `wiki_page` | |
| 41 | | `wiki_page_links` | `[[links]]` of every revision (backlinks, red links) | Wiki | |
| 42 | | `wiki_page_redirects` | every historical path of a page or space (301; 410 when gone) | `openvibe-publishing/seo`, prefix `wiki_page` | |
| 43 | | `wiki_citations` | sources per (page, revision), append-only | `openvibe-publishing/citations`, prefix `wiki` | |
| 44 | | `wiki_infobox_values` | typed infobox values per (page, revision), append-only | Wiki | |
| 45 | | `wiki_permissions` | space roles: owner / editor / viewer | Wiki | |
| 46 | | `wiki_watchers` | who watches a page | Wiki | |
| 47 | | `wiki_ai_proposals` | AI-authored revisions awaiting a person's decision | Wiki + `openvibe-publishing/authorship` | |
| 48 | |
| 49 | The packages add their helper tables under the same prefix (`wiki_page_drafts`, `wiki_page_reviews`, |
| 50 | `wiki_page_attachments`, `wiki_schedule_jobs`, `wiki_discussion_refs`, `wiki_index_revisions`, the |
| 51 | purge audit tables); the SDK outbox keeps `wiki_event_outbox`. Wiki's own `wiki_attachment_origins` |
| 52 | records who attached each Media object and what Media said about it at that moment. |
| 53 | |
| 54 | Wiki also keeps request telemetry in its own `telemetry_samples` table (openvibe-sdk 0.46 telemetry, |
| 55 | the `platform.telemetry-sample@1` shape; migration `0003_telemetry.sql`): each HTTP request is |
| 56 | aggregated by route template, method and status class, never by raw URL, and once a minute one |
| 57 | `http.request` sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is |
| 58 | written. Rows older than 14 days are deleted daily; `/shared` files, static assets, health, readiness |
| 59 | and `/metrics` are not counted. `start()` in [server/index.js](server/index.js) builds the collector |
| 60 | and, with the workers on (production), flushes it every minute and runs the retention, and |
| 61 | [server/app.js](server/app.js) mounts its middleware. |
| 62 | |
| 63 | ## Does not own |
| 64 | |
| 65 | - **Discussion** — OpenVibe.Community owns comment threads; Wiki stores only the thread id |
| 66 | (`wiki_discussion_refs`) and renders or posts through Community's API. |
| 67 | - **Attachments** — OpenVibe.Media owns the bytes; Wiki stores Media object ids (`med_…`) and their |
| 68 | last known state. |
| 69 | - **Source items** — OpenVibe.Sources owns them; a citation stores the item id plus the URL, title, |
| 70 | retrieval time and license read from the item's provenance. |
| 71 | - **The network index** — OpenVibe.Search owns it; Wiki sends `wiki.index_document.*` events. |
| 72 | - **Publication truth by AI** — never. There are no AI provider calls in Wiki: generated wikis run OpenVibe.AI's |
| 73 | registered workflows, and their pages carry the AI disclosure and stay noindex until a person reviews them. |
| 74 | - **Identity** — OpenVibe.Network (subjects `usr_…`, SSO, service principals). |
| 75 | |
| 76 | ## What works |
| 77 | |
| 78 | - **Spaces**: official (created by Network staff, or a service acting as itself) and user spaces |
| 79 | (created by any signed-in person, who becomes owner). Visibility `public` (anyone), `members` |
| 80 | (any signed-in OpenVibe account), `vip` (the space owner's OpenVibe.VIP members, see below), `private` |
| 81 | (people with a role). Roles `owner` (settings, roles, |
| 82 | delete, page visibility), `editor` (write, publish, revert, review proposals), `viewer` (read a |
| 83 | private space). Staff act as owners of official spaces only; they get no silent access to user spaces. |
| 84 | - **VIP spaces and pages** (roadmap WS-K task 8, Contracts 0.64.0): a `vip` space or page is read by the |
| 85 | space's roles and by the viewers OpenVibe.VIP admits as members of the space owner. |
| 86 | - *Who decides:* Wiki asks `vip.resource.policy.evaluate` (`server/integrations/vip.js`). The resource |
| 87 | is `wiki/page/<id>` when the page itself is VIP-only, else `wiki/space/<id>`, with the fallback |
| 88 | `wiki:gated_page`, so a rule the owner sets in VIP wins. |
| 89 | - *When it asks:* before rendering. Route params trigger `access.prepareVip`, answers are cached |
| 90 | (a yes 30 s, a no 10 s), and the JSON export asks authoritatively. |
| 91 | - *Who is refused:* others get a join prompt (403, never cached) linking to the owner's VIP plans. |
| 92 | Every doubt refuses. |
| 93 | - *Where it never shows:* search, sitemaps, feeds, and another viewer's lists. |
| 94 | - *Official spaces* have no VIP owner and refuse `vip`. |
| 95 | - Config: `OV_VIP_INTERNAL_URL`, `OV_VIP_URL`, and `WIKI_VIP_*` for the timeout and cache. |
| 96 | - **Pages**: a tree per space, canonical slugs, rename/move with history-aware 301s (chains |
| 97 | collapse; renaming a space redirects all its pages), deletion answers 410 at every old address. |
| 98 | - **Revisions**: immutable, optimistic concurrency (`expected_revision` → 412 on conflict), word and |
| 99 | line diffs, revert = a new revision copying the old text, infobox and citations. Readers see the |
| 100 | published revision and its history; newer drafts and unapproved AI revisions are editor-only. |
| 101 | - **Editing without JavaScript**: plain HTML forms with preview, "save" and "save and publish", |
| 102 | typed infobox rows (`Label | type | value`), citation rows, keep/drop per existing citation, |
| 103 | conflict handling that keeps the typed text. |
| 104 | - **Infoboxes**: typed values (`text`, `number`, `date`, `url`, `boolean`, `page`, `media`), |
| 105 | validated, never coerced silently. |
| 106 | - **Internal links**: `[[Title]]`, `[[Title|label]]`, `[[space:Title]]`; links follow redirects; |
| 107 | missing (or unreadable) targets are red links — a link to the create form for editors, plain text |
| 108 | for everyone else. "What links here" per page. |
| 109 | - **Citations**: OpenVibe.Sources items (url, title, `retrieved_at`, license from the item's |
| 110 | provenance) or URLs with a required retrieval time; attached to one revision, carried forward |
| 111 | explicitly, never edited; a published revision's sources are fixed. The citation inspector |
| 112 | (`/w/:space/:slug/sources`, linked from the article's sources and every history row) shows one |
| 113 | revision's sources in full — kind, retrieval time, license, quote, the revision that first cited |
| 114 | each one, and what was kept, added, restored or dropped since the previous revision — under the |
| 115 | article's read rules. |
| 116 | - **Media**: attachments by Media object id; a check against Media marks missing or deleted objects |
| 117 | `broken` and the page shows an explicit "no longer available" placeholder instead of an image; an |
| 118 | outage changes nothing (`check_failed`). A periodic check runs when Media is configured. |
| 119 | - **Media across authors and editors**: Media owns read rights (public and unlisted objects are |
| 120 | readable by anyone, a private one only by its owner). A person attaches an object only if they can |
| 121 | read it in Media — missing, deleted and other people's private objects get one answer |
| 122 | (`media.not_readable`), so an id is never confirmed — and only public or unlisted objects are |
| 123 | attached at all (every reader of a page sees its media; Wiki never re-shares a private object under |
| 124 | its own authority). The attachment belongs to the page, with who attached it and the Media |
| 125 | visibility checked then (`wiki_attachment_origins`); later editors revise, revert and publish |
| 126 | without re-attaching and need no rights on it. When Media deletes an object or makes it private, |
| 127 | the next check shows "deleted" or "no longer shared publicly" instead of the image, until Media |
| 128 | shares it again. With Media unreachable or not configured nothing is attached (503). The model is |
| 129 | documented at `attachMedia` in [server/wiki/service.js](server/wiki/service.js). |
| 130 | - **Discussion**: public, published pages get a Community thread (resolved once, stored as a |
| 131 | reference); comments render server-side; signed-in people comment through a form (posted to |
| 132 | Community as that person). Failures show an explicit "could not be loaded" state. |
| 133 | - **Watchers**: watch/unwatch; changes emit `wiki.watch.triggered` with the recipients who may still |
| 134 | read the page (never the actor). No email. |
| 135 | - **Scheduling**: publish at a time (idempotent scheduling, lease-based worker, re-runs are no-ops). |
| 136 | - **AI proposals (seam for OpenVibe.AI)**: a service with `wiki.revision.propose` files a revision |
| 137 | with `ai` authorship (workflow id + run id, stub-provider flag). It stays a pending proposal, |
| 138 | hidden from readers and refused by publish, until a person with the editor role approves it |
| 139 | (approval publishes it) or rejects it. Published AI text carries a disclosure at the item. |
| 140 | - **Discovery (§32)**: server-rendered pages with canonical URLs, robots from the gate, JSON-LD from |
| 141 | real fields only (no invented author, image or date), breadcrumbs; `/sitemap.xml` (index) → |
| 142 | `/sitemaps/spaces.xml`, `/sitemaps/pages-N.xml`; `/feed.atom` and `/feed.json` (recent changes; with nothing listable |
| 143 | yet they are valid feeds with zero entries, never a 404); |
| 144 | `/robots.txt`; `/llms.txt`; a JSON representation of every page at `<page>.json`. |
| 145 | - **IndexNow (openvibe-shared/indexnow)**: with `INDEXNOW_KEY` set, the key file is served at |
| 146 | `/<key>.txt` as `text/plain`; publishing, updating (a new revision, a slug change, a space rename), |
| 147 | unpublishing and deleting a public, indexable page pings `api.indexnow.org` with the page's |
| 148 | canonical URL and `/sitemap.xml` (the module batches and debounces; a failed ping never takes a |
| 149 | publish down). Drafts, private, members, VIP and noindex pages never ping. Unset: off — no key |
| 150 | file, no requests. A key that is not 8–128 hex or alphanumeric is refused by the module: Wiki |
| 151 | warns and runs with IndexNow off rather than failing to start. |
| 152 | - **The gate**: `openvibe-publishing/seo` decides indexing per page with explicit reasons |
| 153 | (policy: at least `WIKI_GATE_MIN_WORDS` words and `WIKI_GATE_MIN_SOURCES` citations; AI text and |
| 154 | AI-assisted imports only after a person's review; owner-requested noindex). Only public, published, indexable pages enter sitemaps and |
| 155 | the Search index; members/private/deleted pages are tombstones and never appear in sitemaps, |
| 156 | feeds or search, and are served `Cache-Control: private, no-store`. |
| 157 | - **Importing a space**: an owner imports a bundle of pages into their space — |
| 158 | `POST /api/v1/spaces/:space/import` or the `/s/:space/import` form (paste the JSON; no |
| 159 | JavaScript needed). The bundle is JSON in the seed file's page shape (`title`, Markdown `body`, |
| 160 | `summary`, `parent` by title, `infobox`, `citations`, `visibility`), validated strictly first |
| 161 | (unknown fields, duplicate slugs, parents, sizes: at most 200 pages, 2 MB, 200,000 characters per |
| 162 | page) and imported in one transaction: one bad page imports nothing. Pages are created exactly like |
| 163 | hand-made ones — drafts unless the bundle says `publish`, the same sanitising renderer, link and |
| 164 | citation records, events and Search documents — with `imported` authorship naming the importer as |
| 165 | the accountable person and where the text comes from; `ai_assisted: true` keeps every page noindex |
| 166 | until a person reviews it. Existing slugs stop the import (409) unless `on_existing: "skip"`. |
| 167 | Twenty imports per hour per address. Format: [server/wiki/import.js](server/wiki/import.js). |
| 168 | - **Vibe a wiki** (`/vibe`, `POST /api/v1/vibes`; [server/wiki/vibe.js](server/wiki/vibe.js)): a signed-in person names |
| 169 | any topic ("Kim Possible", "ancient Rome") and gets a whole wiki about it, in a new user space they own: |
| 170 | 1. *Research*: OpenVibe.AI `research.gather` (MegaSource: Wikipedia, the topic's Fandom wiki, Wikidata, DeepSeek web |
| 171 | search, Commons, Openverse) for the topic. |
| 172 | 2. *Plan*: `wiki.generate_space` names the wiki and plans its pages (the main page first; quick 6, standard 12, big 24). |
| 173 | 3. *Write*: per page, its own research (the page's subject inside the topic) and `wiki.generate_page`. The pictures it |
| 174 | picks are fetched under openvibe-shared/egress ([server/integrations/image-fetch.js](server/integrations/image-fetch.js): |
| 175 | https, public addresses, 8 MB, real JPEG/PNG/GIF/WebP bytes), stored in OpenVibe.Media for the person (public, |
| 176 | with source, licence and author in the object's metadata), placed in the text (`[[media:med_…|caption]]`) or the |
| 177 | infobox (a `media` row), attached, and cited with their licence. The page is imported like any AI-assisted page: |
| 178 | published with the disclosure, noindex until a person reviews it; the main page lists every planned page |
| 179 | ("In this wiki"), and the rest hang under it. |
| 180 | |
| 181 | The job is a `wiki_vibes` row (migration `0004_vibes.sql`) a worker takes under a lease; progress is saved after every |
| 182 | step, so a restart resumes and pages already made are skipped. AI's quota or queue (429) waits for its Retry-After; a |
| 183 | page the sources cannot support is left as a red link. `/vibe/:id` shows it live (and refreshes itself without |
| 184 | JavaScript); `/vibe/:id.json` and `GET /api/v1/vibes/:id` give the same progress. Limits: `WIKI_VIBE_PER_PERSON_DAY` |
| 185 | (3) and `WIKI_VIBE_PER_DAY` (30); `WIKI_VIBE_PAGES_QUICK|STANDARD|BIG`, `WIKI_VIBE_CONCURRENCY` (2 pages at a time). |
| 186 | Needs `OV_AI_INTERNAL_URL` and Media; Network grants `wiki` `ai.run.create`/`ai.run.read` (`wiki.*`, `research.*`) |
| 187 | and `media.object.upload` (namespace `wiki`). Tests: `test/vibe.test.js`. |
| 188 | - **Seed**: `npm run seed` imports the official "OpenVibe" space (`seeds/openvibe.json`): 42 pages |
| 189 | about the network's repositories, summarised from their README/STATUS files at pinned commits, |
| 190 | each cited with a GitHub permalink and retrieval time, authorship recorded as `imported` with |
| 191 | `importedFrom.aiAssisted: true` ("summarised with AI assistance"). Idempotent. Being generated |
| 192 | text, the pages are published and readable with that disclosure ("Not yet reviewed by a person") |
| 193 | but the gate keeps them `noindex` (`ai_generated_unreviewed`) — out of sitemaps, feeds and Search — |
| 194 | until a person reviews each one. |
| 195 | - **Reviewing revisions**: owners and editors (people) review an existing AI-assisted revision from |
| 196 | the history view ("Reviewed — correct" / "Needs changes") or `POST /api/v1/pages/:id/revisions/:n/review`. |
| 197 | The review goes into the append-only review log; for the published revision the gate is |
| 198 | re-evaluated and a changed Search document goes out (with `wiki.page.updated`). Imported rows |
| 199 | written before the flag existed are recognised by their "AI assistance" label, and at boot Wiki |
| 200 | re-sends Search any document whose indexability changed (`reconcileIndex`, idempotent). |
| 201 | |
| 202 | Each public space has its own feed of published pages, `/s/<space>/feed.atom` and `/s/<space>/feed.json`, linked from |
| 203 | the space page (the site-wide `/feed.atom` and `/feed.json` stay). |
| 204 | |
| 205 | Not built yet: page-level talk moderation, media upload from Wiki (attach existing Media objects |
| 206 | only), consuming Media/Sources change events (Media publishes `media.object.deleted` and |
| 207 | `media.object.visibility_changed`, but Wiki subscribes to neither; its checks are pull-based), full-text ranking (Wiki's own `/api/v1/search` is a simple title/text |
| 208 | match; network search is OpenVibe.Search). |
| 209 | |
| 210 | ## Routes |
| 211 | |
| 212 | Pages (SSR, useful without JavaScript): `/`, `/vibe`, `/vibe/:id` (`.json`), `/recent`, `/search?q=`, `/new-space`, `/s/:space`, |
| 213 | `/s/:space/new`, `/s/:space/settings`, `/s/:space/import`, `/s/:space/proposals`, `/w/:space/:slug` (`?rev=N`), |
| 214 | `/w/:space/:slug.json`, `/w/:space/:slug/history`, `/w/:space/:slug/sources` (`?rev=N`), `/w/:space/:slug/diff/:a/:b` (`?mode=line`), |
| 215 | `/w/:space/:slug/edit`, `/w/:space/:slug/revert?to=N`, `/w/:space/:slug/settings`, |
| 216 | `POST /w/:space/:slug/watch|discuss`. Sign-in: `/auth/login|callback|logout|me|refresh|fedcm` |
| 217 | (Network SSO, same session layer as OpenVibe.Community). Legal: `/terms`, `/privacy`, `/dmca`. |
| 218 | Operations: `GET /api/health`, `GET /api/ready` (db required; Network key, Events relay, Community, |
| 219 | Sources, Media optional → degraded), `GET /release.json`, `GET /metrics` (loopback only). |
| 220 | |
| 221 | Caching: anonymous views of public published pages are `public, max-age=60`; signed-in views, |
| 222 | members/private pages, editing surfaces and errors are `private, no-store`. A page a visitor may |
| 223 | not read answers 404, so its existence does not leak. |
| 224 | |
| 225 | Rate limits per address (besides nginx's): sign-in 60 per 15 min, API 300/min, form posts 120 per |
| 226 | 10 min, `/search` 60/min, diffs 30/min, imports 20/hour. Outbound links in community spaces carry |
| 227 | `rel="nofollow ugc noopener"`. The threat review is [docs/threat-review.md](docs/threat-review.md). |
| 228 | |
| 229 | ### Per-actor limits |
| 230 | |
| 231 | `/api/v1` and the editing forms also limit who calls them, once `req.actor` is resolved and before |
| 232 | any work (for a form, before its body is read): `server/http/actor-limits.js`, openvibe-sdk/limits, |
| 233 | roadmap WS-R task 4. Counted: a person as `user:usr_…` (their own token or cookie, or named by a |
| 234 | service in `X-OV-Subject`, or an app's `on_behalf_of`); a first-party service relaying a signed-out |
| 235 | visitor by the address it forwards; a service or app acting as itself (an AI workflow's proposals) |
| 236 | by its principal; a signed-out caller by address. A first-party service reading for itself is not |
| 237 | counted on reads. Past a limit: `429` problem+json `rate_limited` with `Retry-After`, one `[Limits]` |
| 238 | log line and `wiki_rate_limited_total{limit,window}`. A form and the API route that do the same thing |
| 239 | share one budget. |
| 240 | |
| 241 | | Routes (API and form) | Per caller, a minute / an hour | |
| 242 | |---|---| |
| 243 | | API reads | `WIKI_LIMITS_MINUTE` / `WIKI_LIMITS_HOUR` (120 / 3000) | |
| 244 | | Space create (`POST /spaces`, `/new-space`) | 10 / 60 | |
| 245 | | Space settings and roles (`PATCH /spaces/:space`, `PUT …/roles/:subject`, `/s/:space/settings`) | 30 / 300 | |
| 246 | | Page create (`POST /spaces/:space/pages`, `/s/:space/new`) | 30 / 300 | |
| 247 | | Page edit and revert (`POST /pages/:id/revisions`, `/revert`, `/w/…/edit`, `/w/…/revert`) | 30 / 600 | |
| 248 | | Page move, visibility, delete (`PATCH`/`DELETE /pages/:id`, `/w/…/settings`) | 30 / 300 | |
| 249 | | Publish, schedule, unpublish, reviews (API and forms), proposal review | 30 / 300 | |
| 250 | | Citations attach | 30 / 300 | |
| 251 | | AI proposals (`POST /proposals`) | 60 / 1200 | |
| 252 | | Import (API and form) | 5 / 20 | |
| 253 | | Media attach and verify | 20 / 200 | |
| 254 | | Watch | 60 / 600 | |
| 255 | | Discussion comment (`/w/…/discuss`, sent to Community) | 20 / 300 | |
| 256 | | API search and diffs | 30 / 600 each | |
| 257 | |
| 258 | Never limited per actor: `/api/health`, `/api/ready`, `/release.json`, `/metrics`, sign-in, and the |
| 259 | pages people read (the per-address limits above bound them). `test/actor-limits.test.js`; the other |
| 260 | tests boot with `rateLimits: false`, which turns off both kinds. |
| 261 | |
| 262 | ## API `/api/v1` |
| 263 | |
| 264 | People use their Network user JWT (Bearer, or the `ov_token` cookie; cross-site cookie writes are |
| 265 | refused). Services use a Network client-credentials token for audience `openvibe.wiki`; every route |
| 266 | checks one capability, and content writes need the person in `X-OV-Subject` (whose space role |
| 267 | applies). Errors are RFC 9457 problem+json. The full route list is at the top of |
| 268 | [server/http/api.js](server/http/api.js). |
| 269 | |
| 270 | | Capability | Routes | |
| 271 | |---|---| |
| 272 | | `wiki.space.create` | `POST /spaces`, `PATCH /spaces/:space`, `PUT /spaces/:space/roles/:subject` | |
| 273 | | `wiki.page.create` | `POST /spaces/:space/pages`, `POST /spaces/:space/import`, `POST /pages/:id/revisions`, `PATCH`/`DELETE /pages/:id`, `POST /pages/:id/media[/verify]` | |
| 274 | | `wiki.page.read` | `GET /pages/:id`, `/revisions`, `/revisions/:n`, `/diff`, `/revisions/:n/citations`, `GET /proposals/:id` | |
| 275 | | `wiki.revision.propose` | `POST /proposals` | |
| 276 | | `wiki.revision.publish` | `POST /pages/:id/publish`, `/schedule`, `/unpublish`, `POST /pages/:id/revisions/:n/review`, `POST /proposals/:id/review` | |
| 277 | | `wiki.revision.revert` | `POST /pages/:id/revert` | |
| 278 | | `wiki.citation.attach` | `POST /pages/:id/revisions/:n/citations` | |
| 279 | | `wiki.search.query` | `GET /search` | |
| 280 | |
| 281 | The ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposal/) (the plan's |
| 282 | `wiki.search` becomes the three-segment `wiki.search.query`) and are released in openvibe-contracts, |
| 283 | which owns the wiki capability manifests, the service manifest and the `wiki.*` event payload schemas |
| 284 | (this repo pins v0.379.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)). |
| 285 | The proposal for `wiki.revision.publish` also lists the revision review route, which the released |
| 286 | capability names since v0.32.0 (as `wiki.page.create` does the space import route). |
| 287 | |
| 288 | ## Events |
| 289 | |
| 290 | Through the SDK transactional outbox (`openvibe-sdk/events`, table `wiki_event_outbox`), in the same |
| 291 | transaction as the change; the relay publishes to OpenVibe.Events when `EVENTS_URL` is set. |
| 292 | |
| 293 | - `wiki.space.updated` — created, settings, visibility, roles, renamed, deleted |
| 294 | - `wiki.revision.created` — every revision (internal: a revision is a draft until published) |
| 295 | - `wiki.page.published | updated | unpublished | deleted` — publication changes (public only for public, listable pages); `updated` also when a review changes what Search holds |
| 296 | - `wiki.watch.triggered` — recipients who may read the page |
| 297 | - `wiki.index_document.upserted | deleted` — `search.index-document@1` documents and tombstones for |
| 298 | OpenVibe.Search, with a monotonic index revision (`createIndexSequencer`) |
| 299 | |
| 300 | ## Running it |
| 301 | |
| 302 | ```bash |
| 303 | fnm exec --using=22.22.1 npm install |
| 304 | cp .env.example .env |
| 305 | fnm exec --using=22.22.1 npm run seed # the official "OpenVibe" space |
| 306 | fnm exec --using=22.22.1 npm run dev # http://localhost:4800 |
| 307 | fnm exec --using=22.22.1 npm test # every test/*.test.js on PGlite (PostgreSQL in-process), no network |
| 308 | eval "$(node_modules/openvibe-sdk/scripts/test-services.sh up)" && npm run test:pg # the same through PgBouncer |
| 309 | ``` |
| 310 | |
| 311 | Without `DATABASE_URL`, development uses an embedded PGlite database in `data/pglite`. |
| 312 | |
| 313 | Production: `/opt/openvibe.wiki`, env `/etc/openvibe/wiki.env`, unit |
| 314 | [deploy/systemd/openvibe-wiki.service](deploy/systemd/openvibe-wiki.service), database `ov_wiki` |
| 315 | (`DATABASE_URL` through PgBouncer; migrations on `DATABASE_DIRECT_URL`), nginx [deploy/nginx/openvibe.wiki.conf](deploy/nginx/openvibe.wiki.conf). |
| 316 | |
| 317 | ## Depends on |
| 318 | |
| 319 | - PostgreSQL 18 and Valkey 9 (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through |
| 320 | `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process). |
| 321 | - `openvibe-publishing` v1.7.0 (async PostgreSQL stores, ingest and publication chassis), `openvibe-contracts` |
| 322 | v0.379.0, `openvibe-shared` v4.241.0 |
| 323 | (Frame, release, metrics, readiness, SEO helpers, legal pages), `openvibe-sdk` v0.161.0 (db, auth, PostgreSQL |
| 324 | events outbox, per-actor limits, account export and deletion, testing) — pinned release tarballs. |
| 325 | - OpenVibe.Network (SSO, JWKS, service principal `wiki`), OpenVibe.Events, OpenVibe.Community, |
| 326 | OpenVibe.Sources, OpenVibe.Media, OpenVibe.VIP (VIP spaces and pages), OpenVibe.Search (consumer of |
| 327 | the index events). All but the Network key are optional at runtime and degrade to explicit failure |
| 328 | states. |
| 329 | - OpenVibe.AI: generated wikis (`OV_AI_INTERNAL_URL`; research.gather, wiki.generate_space, wiki.generate_page). AI |
| 330 | proposals still arrive through the proposal API. |
| 331 | - Account export and deletion (ADR-033): Network grants `events.subscription.manage` (openvibe.events) for the two |
| 332 | subscriptions created at boot, then, last and once the release is live, `network.account.export.contribute` and |
| 333 | `network.account.deletion.confirm` (openvibe.network). |
| 334 | |
| 335 | ### Account export and deletion (ADR-033) |
| 336 | |
| 337 | `network.account.export_requested` and `network.account.deleted` arrive at `POST /internal/events`. The route is |
| 338 | loopback-only (nginx answers 404 for `/internal/`, and the handler refuses a forwarded request) and signed with |
| 339 | `WIKI_EVENTS_SECRET`. They are answered by `server/wiki/account-data.js` over `openvibe-sdk/account-data`, with one |
| 340 | receipt per export and deletion in `account_data_events`. |
| 341 | |
| 342 | - **Export:** the person's spaces, pages, revisions, AI proposals, roles, watches, drafts and reviews. |
| 343 | - **Deleted:** a user space they own is deleted the way its owner would delete it (`deleteSpace` as the system actor: |
| 344 | unpublished, out of Search and the sitemap, answering 410). Their roles, watches, drafts and wiki.projects state go |
| 345 | too. |
| 346 | - **Authorless:** what they wrote in other spaces stays, with their id removed (`created_by`, `proposed_by` and |
| 347 | `attached_by` become `deleted`). Revisions and citations are append-only, so only the erasure transaction may clear |
| 348 | a revision's author (and their id in `meta.authorship.authors`) or a citation's `attached_by` |
| 349 | (`wiki.account_erasure`, migration `0002_account_erasure.sql`); the text and sources never change. |
| 350 | - **Kept:** page reviews, since a person's approval is what lets reviewed text stay published. |
| 351 | |
| 352 | ## Capabilities |
| 353 | |
| 354 | Implemented here (the service manifest's `capabilities`, audience `openvibe.wiki`, one per route; |
| 355 | routes under [API](#api-apiv1)): `wiki.space.create`, `wiki.page.create`, `wiki.page.read`, |
| 356 | `wiki.revision.propose`, `wiki.revision.publish`, `wiki.revision.revert`, `wiki.citation.attach` and |
| 357 | `wiki.search.query`. |
| 358 | |
| 359 | Called elsewhere, as the service principal `svc:wiki` (client credentials, one token per audience; |
| 360 | [server/integrations/platform.js](server/integrations/platform.js)): |
| 361 | |
| 362 | | Service | Grant | Why | |
| 363 | |---|---|---| |
| 364 | | OpenVibe.Events | `events.event.publish` | the outbox relay | |
| 365 | | OpenVibe.Community | `community.comment.write`, `community.comment.moderate` | comment threads by reference; hiding the thread of a page that stops being public | |
| 366 | | OpenVibe.Sources | `sources.item.read` | citations that name a Sources item | |
| 367 | | OpenVibe.Media | `media.object.read` (namespace `WIKI_MEDIA_APP`) | attached objects and their re-verification | |
| 368 | | OpenVibe.VIP | `vip.resource.policy.evaluate` | who may read a `vip` space or page | |
| 369 | |
| 370 | ## Acceptance (tested in `test/`) |
| 371 | |
| 372 | - create → edit → publish → revise → diff → revert keeps an immutable lineage (`revisions.test.js`) |
| 373 | - citations and infobox values stay attached to the exact revision that used them (`revisions.test.js`); |
| 374 | the citation inspector shows them per revision without JavaScript and leaks nothing the article |
| 375 | would not show (`citation-inspector.test.js`) |
| 376 | - scheduled publication is idempotent across worker restarts (`schedule.test.js`) |
| 377 | - a private, members-only or deleted page leaves sitemaps, feeds and the Search index (tombstone) |
| 378 | and is served `Cache-Control: private`; renames 301, deletions 410 (`visibility.test.js`) |
| 379 | - `/feed.atom` is a valid Atom feed with zero entries while nothing is listable (`feeds.test.js`) |
| 380 | - a public page is useful with JavaScript disabled, and so is editing (`nojs.test.js`) |
| 381 | - a deleted or missing Media object renders an explicit broken-asset state (`integrations.test.js`) |
| 382 | - Media permissions survive the author/editor handoff: an editor keeps the author's attachments, |
| 383 | cannot attach an object only the author can read, and an object Media deletes or makes private is |
| 384 | shown as broken or withheld on the public page (`media-handoff.test.js`) |
| 385 | - permissions are enforced; visitors without SSO read public content only; AI proposals need a |
| 386 | person's approval (`permissions.test.js`) |
| 387 | - only owners import into a space; invalid, oversized and colliding bundles are refused and any |
| 388 | failure imports nothing (`import.test.js`) |
| 389 | - every event is a valid `events.event-envelope@1` and every index document a valid |
| 390 | `search.index-document@1` (`visibility.test.js`); the proposals validate against the contracts |
| 391 | schemas (`proposals.test.js`); user text is escaped everywhere (`content.test.js`) |
| 392 | - IndexNow: off without `INDEXNOW_KEY` (no key route, nothing sent); with one the key file is served |
| 393 | at `/<key>.txt` as `text/plain` and a publish, unpublish or delete of an indexable page pings the |
| 394 | page path and the sitemap; a draft, a noindex page and a page the gate will not index never ping |
| 395 | (`indexnow.test.js`) |
| 396 | |
| 397 | ## Security |
| 398 | |
| 399 | Reporting a vulnerability: [SECURITY.md](SECURITY.md). The written threat review, with code |
| 400 | references and the gaps that remain: [docs/threat-review.md](docs/threat-review.md). |
| 401 | |
| 402 | - **Auth.** People use a Network JWT (Bearer or the `ov_token` cookie; cross-site cookie writes are |
| 403 | refused); services use client-credentials tokens for audience `openvibe.wiki`, one capability per |
| 404 | route, and content writes need the person in `X-OV-Subject`, whose space role applies. Visitors |
| 405 | without SSO read public content only; AI proposals need a person's approval. |
| 406 | - **Private data.** Private, members-only, VIP and deleted pages leave sitemaps, feeds and Search |
| 407 | (tombstones) and are served `Cache-Control: private`; a VIP refusal is never cached; Media objects |
| 408 | the reader may not see are withheld. User text is escaped everywhere. |
| 409 | - **Egress.** Wiki calls only its configured Network, Events, Community, Sources, Media and VIP hosts; |
| 410 | it never fetches a URL a user chose. |
| 411 | - **Secrets.** `OV_OAUTH_CLIENT_SECRET` lives in `/etc/openvibe/wiki.env` (0600). nginx answers |
| 412 | `/metrics` with 404 and caps import bodies. |
| 413 | |
| 414 | ## Deploy |
| 415 | |
| 416 | Production deploys with `sudo ovhost deploy wiki` on the host (strategy `git-checkout`: fetch, |
| 417 | fast-forward `/opt/openvibe.wiki`, install on a lockfile change, restart, wait for `/api/ready`). |
| 418 | The unit is `openvibe-wiki.service` on `127.0.0.1:4800`, the env file `/etc/openvibe/wiki.env`. The database is |
| 419 | `ov_wiki` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh wiki` writes its settings); |
| 420 | the release migrates it at boot. nginx serves `openvibe.wiki` from |
| 421 | [deploy/nginx/openvibe.wiki.conf](deploy/nginx/openvibe.wiki.conf). |
| 422 | Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the |
| 423 | restart; afterwards `sudo ovhost rollback wiki --to <sha>`. Nothing blocks a rollback: the schema |
| 424 | code only adds tables and columns. |
| 425 | |
| 426 | ## Launch rule |
| 427 | |
| 428 | This repository does not make the product real on its own. `openvibe.wiki` kept its placeholder |
| 429 | page on [OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until the following |
| 430 | held (plan §12.12); the launch release went out on 2026-09-23: |
| 431 | |
| 432 | 1. an owning runtime with health/readiness endpoints and observability — **built** (`/api/health`, `/api/ready`, `/metrics`); |
| 433 | 2. canonical identity/auth integration (Network subjects, a scoped service principal) — **built**; principal `wiki` and its grants are provisioned in production; |
| 434 | 3. server-rendered public routes useful without JavaScript — **built**; |
| 435 | 4. real persistence and end-to-end workflows — **built** and deployed; `ovhost drill wiki` restored it on the production host on 2026-09-23; |
| 436 | 5. capability and event registration against OpenVibe.Contracts — **released** (capabilities and service manifest in v0.17.0; the `wiki.*` event payload schemas are in the pinned release and every event this service emits validates against them, `test/event-contracts.test.js`); |
| 437 | 6. a migration/seed strategy, a security/threat review, sitemap/robots/feed behaviour — seed and discovery **built**; the written threat review is [docs/threat-review.md](docs/threat-review.md) (2026-09-23: mitigations with code references, the gaps fixed in that pass with tests in `test/threat-review.test.js`, and the gaps that remain with their owners); |
| 438 | 7. acceptance tests proving the advertised functionality — **built** (`npm test`). |
| 439 | |
| 440 | The launch release removed `openvibe.wiki` from `OpenVibe.Sites/sites.json`, switched routing to this |
| 441 | service and opened its Network hub entry in the same release. A placeholder is never counted as an |
| 442 | implemented service. Still open: a person's review of the 42 seed pages (until then they stay |
| 443 | `noindex`). |
| 444 | |
| 445 | --- |
| 446 | |
| 447 | Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers). |
| 448 | |
| 449 | <!-- versions:start --> |
| 450 | - openvibe-contracts: v0.419.0 |
| 451 | - openvibe-sdk: v0.161.0 |
| 452 | - openvibe-shared: v4.241.0 |
| 453 | - openvibe-publishing: v1.7.0 |
| 454 | <!-- versions:end --> |
| 455 |