openvibers/openvibe.wiki README.md

README.md

main 33.2 KB · 455 lines · markdown

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)).
10The only content is the official space's 42 AI-assisted seed pages, one for each network repository; no person has reviewed them yet,
11so 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
18The knowledge product of the network: wikis (official editorial wikis and community ones, called spaces in the code and API), page
19trees with canonical slugs, immutable revisions with diff and revert, manual editing by permitted
20people, typed infoboxes, `[[internal links]]`, citations attached to the revision that used them,
21Media attachments and Community discussion. AI proposes revisions; a person approves them; Wiki
22publishes. Search indexes only published, public state.
23
24It 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,
26citations, redirects, the indexability gate, feeds, sitemaps, JSON-LD, authorship, scheduling,
27media references, discussion references and the Search index hooks all come from there. Wiki owns
28its publication state; the packages supply the mechanics.
29
30Sitemap entries carry lastmod from each published page or space's latest change.
31
32## Owns
33
34The 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
49The 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
51purge audit tables); the SDK outbox keeps `wiki_event_outbox`. Wiki's own `wiki_attachment_origins`
52records who attached each Media object and what Media said about it at that moment.
53
54Wiki also keeps request telemetry in its own `telemetry_samples` table (openvibe-sdk 0.46 telemetry,
55the `platform.telemetry-sample@1` shape; migration `0003_telemetry.sql`): each HTTP request is
56aggregated 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
58written. Rows older than 14 days are deleted daily; `/shared` files, static assets, health, readiness
59and `/metrics` are not counted. `start()` in [server/index.js](server/index.js) builds the collector
60and, 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
202Each public space has its own feed of published pages, `/s/<space>/feed.atom` and `/s/<space>/feed.json`, linked from
203the space page (the site-wide `/feed.atom` and `/feed.json` stay).
204
205Not built yet: page-level talk moderation, media upload from Wiki (attach existing Media objects
206only), 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
208match; network search is OpenVibe.Search).
209
210## Routes
211
212Pages (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`.
218Operations: `GET /api/health`, `GET /api/ready` (db required; Network key, Events relay, Community,
219Sources, Media optional → degraded), `GET /release.json`, `GET /metrics` (loopback only).
220
221Caching: anonymous views of public published pages are `public, max-age=60`; signed-in views,
222members/private pages, editing surfaces and errors are `private, no-store`. A page a visitor may
223not read answers 404, so its existence does not leak.
224
225Rate limits per address (besides nginx's): sign-in 60 per 15 min, API 300/min, form posts 120 per
22610 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
232any work (for a form, before its body is read): `server/http/actor-limits.js`, openvibe-sdk/limits,
233roadmap WS-R task 4. Counted: a person as `user:usr_…` (their own token or cookie, or named by a
234service in `X-OV-Subject`, or an app's `on_behalf_of`); a first-party service relaying a signed-out
235visitor by the address it forwards; a service or app acting as itself (an AI workflow's proposals)
236by its principal; a signed-out caller by address. A first-party service reading for itself is not
237counted on reads. Past a limit: `429` problem+json `rate_limited` with `Retry-After`, one `[Limits]`
238log line and `wiki_rate_limited_total{limit,window}`. A form and the API route that do the same thing
239share 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
258Never limited per actor: `/api/health`, `/api/ready`, `/release.json`, `/metrics`, sign-in, and the
259pages people read (the per-address limits above bound them). `test/actor-limits.test.js`; the other
260tests boot with `rateLimits: false`, which turns off both kinds.
261
262## API `/api/v1`
263
264People use their Network user JWT (Bearer, or the `ov_token` cookie; cross-site cookie writes are
265refused). Services use a Network client-credentials token for audience `openvibe.wiki`; every route
266checks one capability, and content writes need the person in `X-OV-Subject` (whose space role
267applies). 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
281The 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,
283which 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)).
285The proposal for `wiki.revision.publish` also lists the revision review route, which the released
286capability names since v0.32.0 (as `wiki.page.create` does the space import route).
287
288## Events
289
290Through the SDK transactional outbox (`openvibe-sdk/events`, table `wiki_event_outbox`), in the same
291transaction 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
303fnm exec --using=22.22.1 npm install
304cp .env.example .env
305fnm exec --using=22.22.1 npm run seed # the official "OpenVibe" space
306fnm exec --using=22.22.1 npm run dev # http://localhost:4800
307fnm exec --using=22.22.1 npm test # every test/*.test.js on PGlite (PostgreSQL in-process), no network
308eval "$(node_modules/openvibe-sdk/scripts/test-services.sh up)" && npm run test:pg # the same through PgBouncer
309```
310
311Without `DATABASE_URL`, development uses an embedded PGlite database in `data/pglite`.
312
313Production: `/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
338loopback-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
340receipt 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
354Implemented here (the service manifest's `capabilities`, audience `openvibe.wiki`, one per route;
355routes 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
359Called 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
399Reporting a vulnerability: [SECURITY.md](SECURITY.md). The written threat review, with code
400references 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
416Production deploys with `sudo ovhost deploy wiki` on the host (strategy `git-checkout`: fetch,
417fast-forward `/opt/openvibe.wiki`, install on a lockfile change, restart, wait for `/api/ready`).
418The 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);
420the release migrates it at boot. nginx serves `openvibe.wiki` from
421[deploy/nginx/openvibe.wiki.conf](deploy/nginx/openvibe.wiki.conf).
422Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
423restart; afterwards `sudo ovhost rollback wiki --to <sha>`. Nothing blocks a rollback: the schema
424code only adds tables and columns.
425
426## Launch rule
427
428This repository does not make the product real on its own. `openvibe.wiki` kept its placeholder
429page on [OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until the following
430held (plan §12.12); the launch release went out on 2026-09-23:
431
4321. an owning runtime with health/readiness endpoints and observability — **built** (`/api/health`, `/api/ready`, `/metrics`);
4332. canonical identity/auth integration (Network subjects, a scoped service principal) — **built**; principal `wiki` and its grants are provisioned in production;
4343. server-rendered public routes useful without JavaScript — **built**;
4354. real persistence and end-to-end workflows — **built** and deployed; `ovhost drill wiki` restored it on the production host on 2026-09-23;
4365. 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`);
4376. 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);
4387. acceptance tests proving the advertised functionality — **built** (`npm test`).
439
440The launch release removed `openvibe.wiki` from `OpenVibe.Sites/sites.json`, switched routing to this
441service and opened its Network hub entry in the same release. A placeholder is never counted as an
442implemented service. Still open: a person's review of the 42 seed pages (until then they stay
443`noindex`).
444
445---
446
447Part 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