Mirror of github.com/OpenVibers/OpenVibe.Wiki
  • JavaScript 95%
  • PLpgSQL 2.6%
  • CSS 2.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
OpenVibers cecc2c9859
openvibe-contracts v0.216.0 (was v0.215.0) (#117)
Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v0.216.0 release.
2026-10-27 05:01:48 -07:00
.github/workflows Pin Shared workflows to aa570e4 and enable the docs-status check (#47) 2026-10-14 09:43:29 -07:00
deploy Wiki: nginx refuses /internal/ and /metrics case-insensitively (#24) 2026-10-09 18:57:27 -07:00
docs The SQLite era is gone (on PostgreSQL since 2026-09-28; no compatibility kept): the one-time migrate-to-postgres script, WIKI_DB_PATH/dbPath (config, .env.example, systemd unit, test helpers), the unused better-sqlite3 dependency and its lockfile entries, and docs, docstrings and STATUS lines about SQLite or its rollback window. Reviewed independently (VERDICT: SHIP). npm test 32/32 test files passed. 2026-09-28 20:05:32 -07:00
migrations Vibe a wiki: any topic → a whole wiki with pictures and sources (#104) 2026-10-25 00:51:08 -07:00
public/css Section headings flow beside a tall infobox again (no clear: both); a picture right under a heading starts at its top (#105) 2026-10-25 00:57:15 -07:00
scripts Wiki: the official space covers all 35 products, refreshed from today's READMEs; seed --revise (#40) 2026-10-11 16:15:23 -07:00
seeds The wiki's home shows what is in it: the numbers, the OpenVibe wiki shelved by site group, the most linked pages, recent revisions, how a page comes together, questions and links around the site (#93) 2026-10-22 07:34:56 -07:00
server Home: the "Part of OpenVibe" section (Shared 4.13) (#109) 2026-10-25 18:49:31 -07:00
test openvibe-shared v4.15.0 (was v4.14.0) (#112) 2026-10-26 19:48:14 -07:00
.env.example Vibe a wiki: any topic → a whole wiki with pictures and sources (#104) 2026-10-25 00:51:08 -07:00
.gitignore Pin openvibe-contracts v0.96.0 and refresh the docs it names (#9) 2026-10-04 18:19:17 -07:00
CHANGELOG.md Wiki: account export and deletion (ADR-033) — a person's own spaces deleted as their owner would, roles, watches and drafts removed, contributions made authorless (append-only rows only inside the erasure transaction), reviews kept; loopback /internal/events; subscriptions at boot (SDK v0.37.0) (#18) 2026-10-08 19:57:42 -07:00
LICENSE Charter: OpenVibe.Wiki as defined in the OpenVibe realignment plan (placeholder, no code yet) 2026-09-21 09:20:04 -07:00
package-lock.json openvibe-contracts v0.216.0 (was v0.215.0) (#117) 2026-10-27 05:01:48 -07:00
package.json openvibe-contracts v0.216.0 (was v0.215.0) (#117) 2026-10-27 05:01:48 -07:00
README.md openvibe-contracts v0.216.0 (was v0.215.0) (#117) 2026-10-27 05:01:48 -07:00
SECURITY.md SECURITY.md: how to report a vulnerability ([email protected], 7-day reply, scope, supported versions) 2026-09-23 19:27:58 -07:00
STATUS.json openvibe-contracts v0.216.0 (was v0.215.0) (#117) 2026-10-27 05:01:48 -07:00

OpenVibe.Wiki

Wikis with page trees, revisions, citations, media and discussion — editable together, source-backed.

Status: alpha (roadmap Wave 16, Wiki half). Public at https://openvibe.wiki since 2026-09-23 (the launch release also removed the domain from OpenVibe.Sites). The only content is the official space's 42 AI-assisted seed pages, one for each network repository; no person has reviewed them yet, so every page is noindex and none is in Search. Domain: openvibe.wiki · Port: 4800 · Service id: wiki Plan: OpenVibe End-to-End Realignment & Implementation Plan, revision 3 — roadmap Wave 16, §15.13, §29, §32. License: AGPL-3.0 (same as every OpenVibe service).

Purpose

The knowledge product of the network: wikis (official editorial wikis and community ones, called spaces in the code and API), page trees with canonical slugs, immutable revisions with diff and revert, manual editing by permitted people, typed infoboxes, [[internal links]], citations attached to the revision that used them, Media attachments and Community discussion. AI proposes revisions; a person approves them; Wiki publishes. Search indexes only published, public state.

It is the first full consumer of the shared publishing packages (openvibe-publishing v0.2.1, ADR-019): revisions, citations, redirects, the indexability gate, feeds, sitemaps, JSON-LD, authorship, scheduling, media references, discussion references and the Search index hooks all come from there. Wiki owns its publication state; the packages supply the mechanics.

Sitemap entries carry lastmod from each published page or space's latest change.

Owns

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/):

Table What Mechanics
wiki_spaces spaces: official / user, public / members / private Wiki
wiki_pages canonical page identity, slug, tree (parent_id), publication state, published revision Wiki
wiki_page_revisions immutable revisions (UPDATE/DELETE refused by PL/pgSQL triggers) openvibe-publishing/revisions, prefix wiki_page
wiki_page_links [[links]] of every revision (backlinks, red links) Wiki
wiki_page_redirects every historical path of a page or space (301; 410 when gone) openvibe-publishing/seo, prefix wiki_page
wiki_citations sources per (page, revision), append-only openvibe-publishing/citations, prefix wiki
wiki_infobox_values typed infobox values per (page, revision), append-only Wiki
wiki_permissions space roles: owner / editor / viewer Wiki
wiki_watchers who watches a page Wiki
wiki_ai_proposals AI-authored revisions awaiting a person's decision Wiki + openvibe-publishing/authorship

The packages add their helper tables under the same prefix (wiki_page_drafts, wiki_page_reviews, wiki_page_attachments, wiki_schedule_jobs, wiki_discussion_refs, wiki_index_revisions, the purge audit tables); the SDK outbox keeps wiki_event_outbox. Wiki's own wiki_attachment_origins records who attached each Media object and what Media said about it at that moment.

Wiki also keeps request telemetry in its own telemetry_samples table (openvibe-sdk 0.46 telemetry, the platform.telemetry-sample@1 shape; migration 0003_telemetry.sql): each HTTP request is aggregated by route template, method and status class, never by raw URL, and once a minute one http.request sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written. Rows older than 14 days are deleted daily; /shared files, static assets, health, readiness and /metrics are not counted. start() in server/index.js builds the collector and, with the workers on (production), flushes it every minute and runs the retention, and server/app.js mounts its middleware.

Does not own

  • Discussion — OpenVibe.Community owns comment threads; Wiki stores only the thread id (wiki_discussion_refs) and renders or posts through Community's API.
  • Attachments — OpenVibe.Media owns the bytes; Wiki stores Media object ids (med_…) and their last known state.
  • Source items — OpenVibe.Sources owns them; a citation stores the item id plus the URL, title, retrieval time and license read from the item's provenance.
  • The network index — OpenVibe.Search owns it; Wiki sends wiki.index_document.* events.
  • Publication truth by AI — never. There are no AI provider calls in Wiki: generated wikis run OpenVibe.AI's registered workflows, and their pages carry the AI disclosure and stay noindex until a person reviews them.
  • Identity — OpenVibe.Network (subjects usr_…, SSO, service principals).

What works

  • Spaces: official (created by Network staff, or a service acting as itself) and user spaces (created by any signed-in person, who becomes owner). Visibility public (anyone), members (any signed-in OpenVibe account), vip (the space owner's OpenVibe.VIP members, see below), private (people with a role). Roles owner (settings, roles, delete, page visibility), editor (write, publish, revert, review proposals), viewer (read a private space). Staff act as owners of official spaces only; they get no silent access to user spaces.

  • VIP spaces and pages (roadmap WS-K task 8, Contracts 0.64.0): a vip space or page is read by the space's roles and by the viewers OpenVibe.VIP admits as members of the space owner.

    • Who decides: Wiki asks vip.resource.policy.evaluate (server/integrations/vip.js). The resource is wiki/page/<id> when the page itself is VIP-only, else wiki/space/<id>, with the fallback wiki:gated_page, so a rule the owner sets in VIP wins.
    • When it asks: before rendering. Route params trigger access.prepareVip, answers are cached (a yes 30 s, a no 10 s), and the JSON export asks authoritatively.
    • Who is refused: others get a join prompt (403, never cached) linking to the owner's VIP plans. Every doubt refuses.
    • Where it never shows: search, sitemaps, feeds, and another viewer's lists.
    • Official spaces have no VIP owner and refuse vip.
    • Config: OV_VIP_INTERNAL_URL, OV_VIP_URL, and WIKI_VIP_* for the timeout and cache.
  • Pages: a tree per space, canonical slugs, rename/move with history-aware 301s (chains collapse; renaming a space redirects all its pages), deletion answers 410 at every old address.

  • Revisions: immutable, optimistic concurrency (expected_revision → 412 on conflict), word and line diffs, revert = a new revision copying the old text, infobox and citations. Readers see the published revision and its history; newer drafts and unapproved AI revisions are editor-only.

  • Editing without JavaScript: plain HTML forms with preview, "save" and "save and publish", typed infobox rows (Label | type | value), citation rows, keep/drop per existing citation, conflict handling that keeps the typed text.

  • Infoboxes: typed values (text, number, date, url, boolean, page, media), validated, never coerced silently.

  • Internal links: [[Title]], [[Title|label]], [[space:Title]]; links follow redirects; missing (or unreadable) targets are red links — a link to the create form for editors, plain text for everyone else. "What links here" per page.

  • Citations: OpenVibe.Sources items (url, title, retrieved_at, license from the item's provenance) or URLs with a required retrieval time; attached to one revision, carried forward explicitly, never edited; a published revision's sources are fixed. The citation inspector (/w/:space/:slug/sources, linked from the article's sources and every history row) shows one revision's sources in full — kind, retrieval time, license, quote, the revision that first cited each one, and what was kept, added, restored or dropped since the previous revision — under the article's read rules.

  • Media: attachments by Media object id; a check against Media marks missing or deleted objects broken and the page shows an explicit "no longer available" placeholder instead of an image; an outage changes nothing (check_failed). A periodic check runs when Media is configured.

  • Media across authors and editors: Media owns read rights (public and unlisted objects are readable by anyone, a private one only by its owner). A person attaches an object only if they can read it in Media — missing, deleted and other people's private objects get one answer (media.not_readable), so an id is never confirmed — and only public or unlisted objects are attached at all (every reader of a page sees its media; Wiki never re-shares a private object under its own authority). The attachment belongs to the page, with who attached it and the Media visibility checked then (wiki_attachment_origins); later editors revise, revert and publish without re-attaching and need no rights on it. When Media deletes an object or makes it private, the next check shows "deleted" or "no longer shared publicly" instead of the image, until Media shares it again. With Media unreachable or not configured nothing is attached (503). The model is documented at attachMedia in server/wiki/service.js.

  • Discussion: public, published pages get a Community thread (resolved once, stored as a reference); comments render server-side; signed-in people comment through a form (posted to Community as that person). Failures show an explicit "could not be loaded" state.

  • Watchers: watch/unwatch; changes emit wiki.watch.triggered with the recipients who may still read the page (never the actor). No email.

  • Scheduling: publish at a time (idempotent scheduling, lease-based worker, re-runs are no-ops).

  • AI proposals (seam for OpenVibe.AI): a service with wiki.revision.propose files a revision with ai authorship (workflow id + run id, stub-provider flag). It stays a pending proposal, hidden from readers and refused by publish, until a person with the editor role approves it (approval publishes it) or rejects it. Published AI text carries a disclosure at the item.

  • Discovery (§32): server-rendered pages with canonical URLs, robots from the gate, JSON-LD from real fields only (no invented author, image or date), breadcrumbs; /sitemap.xml (index) → /sitemaps/spaces.xml, /sitemaps/pages-N.xml; /feed.atom and /feed.json (recent changes; with nothing listable yet they are valid feeds with zero entries, never a 404); /robots.txt; /llms.txt; a JSON representation of every page at <page>.json.

  • IndexNow (openvibe-shared/indexnow): with INDEXNOW_KEY set, the key file is served at /<key>.txt as text/plain; publishing, updating (a new revision, a slug change, a space rename), unpublishing and deleting a public, indexable page pings api.indexnow.org with the page's canonical URL and /sitemap.xml (the module batches and debounces; a failed ping never takes a publish down). Drafts, private, members, VIP and noindex pages never ping. Unset: off — no key file, no requests. A key that is not 8–128 hex or alphanumeric is refused by the module: Wiki warns and runs with IndexNow off rather than failing to start.

  • The gate: openvibe-publishing/seo decides indexing per page with explicit reasons (policy: at least WIKI_GATE_MIN_WORDS words and WIKI_GATE_MIN_SOURCES citations; AI text and AI-assisted imports only after a person's review; owner-requested noindex). Only public, published, indexable pages enter sitemaps and the Search index; members/private/deleted pages are tombstones and never appear in sitemaps, feeds or search, and are served Cache-Control: private, no-store.

  • Importing a space: an owner imports a bundle of pages into their space — POST /api/v1/spaces/:space/import or the /s/:space/import form (paste the JSON; no JavaScript needed). The bundle is JSON in the seed file's page shape (title, Markdown body, summary, parent by title, infobox, citations, visibility), validated strictly first (unknown fields, duplicate slugs, parents, sizes: at most 200 pages, 2 MB, 200,000 characters per page) and imported in one transaction: one bad page imports nothing. Pages are created exactly like hand-made ones — drafts unless the bundle says publish, the same sanitising renderer, link and citation records, events and Search documents — with imported authorship naming the importer as the accountable person and where the text comes from; ai_assisted: true keeps every page noindex until a person reviews it. Existing slugs stop the import (409) unless on_existing: "skip". Twenty imports per hour per address. Format: server/wiki/import.js.

  • Vibe a wiki (/vibe, POST /api/v1/vibes; server/wiki/vibe.js): a signed-in person names any topic ("Kim Possible", "ancient Rome") and gets a whole wiki about it, in a new user space they own:

    1. Research: OpenVibe.AI research.gather (MegaSource: Wikipedia, the topic's Fandom wiki, Wikidata, DeepSeek web search, Commons, Openverse) for the topic.
    2. Plan: wiki.generate_space names the wiki and plans its pages (the main page first; quick 6, standard 12, big 24).
    3. Write: per page, its own research (the page's subject inside the topic) and wiki.generate_page. The pictures it picks are fetched under openvibe-shared/egress (server/integrations/image-fetch.js: https, public addresses, 8 MB, real JPEG/PNG/GIF/WebP bytes), stored in OpenVibe.Media for the person (public, with source, licence and author in the object's metadata), placed in the text ([[media:med_…|caption]]) or the infobox (a media row), attached, and cited with their licence. The page is imported like any AI-assisted page: published with the disclosure, noindex until a person reviews it; the main page lists every planned page ("In this wiki"), and the rest hang under it.

    The job is a wiki_vibes row (migration 0004_vibes.sql) a worker takes under a lease; progress is saved after every step, so a restart resumes and pages already made are skipped. AI's quota or queue (429) waits for its Retry-After; a page the sources cannot support is left as a red link. /vibe/:id shows it live (and refreshes itself without JavaScript); /vibe/:id.json and GET /api/v1/vibes/:id give the same progress. Limits: WIKI_VIBE_PER_PERSON_DAY (3) and WIKI_VIBE_PER_DAY (30); WIKI_VIBE_PAGES_QUICK|STANDARD|BIG, WIKI_VIBE_CONCURRENCY (2 pages at a time). Needs OV_AI_INTERNAL_URL and Media; Network grants wiki ai.run.create/ai.run.read (wiki.*, research.*) and media.object.upload (namespace wiki). Tests: test/vibe.test.js.

  • Seed: npm run seed imports the official "OpenVibe" space (seeds/openvibe.json): 42 pages about the network's repositories, summarised from their README/STATUS files at pinned commits, each cited with a GitHub permalink and retrieval time, authorship recorded as imported with importedFrom.aiAssisted: true ("summarised with AI assistance"). Idempotent. Being generated text, the pages are published and readable with that disclosure ("Not yet reviewed by a person") but the gate keeps them noindex (ai_generated_unreviewed) — out of sitemaps, feeds and Search — until a person reviews each one.

  • Reviewing revisions: owners and editors (people) review an existing AI-assisted revision from the history view ("Reviewed — correct" / "Needs changes") or POST /api/v1/pages/:id/revisions/:n/review. The review goes into the append-only review log; for the published revision the gate is re-evaluated and a changed Search document goes out (with wiki.page.updated). Imported rows written before the flag existed are recognised by their "AI assistance" label, and at boot Wiki re-sends Search any document whose indexability changed (reconcileIndex, idempotent).

Each public space has its own feed of published pages, /s/<space>/feed.atom and /s/<space>/feed.json, linked from the space page (the site-wide /feed.atom and /feed.json stay).

Not built yet: page-level talk moderation, media upload from Wiki (attach existing Media objects only), consuming Media/Sources change events (Media publishes media.object.deleted and 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 match; network search is OpenVibe.Search).

Routes

Pages (SSR, useful without JavaScript): /, /vibe, /vibe/:id (.json), /recent, /search?q=, /new-space, /s/:space, /s/:space/new, /s/:space/settings, /s/:space/import, /s/:space/proposals, /w/:space/:slug (?rev=N), /w/:space/:slug.json, /w/:space/:slug/history, /w/:space/:slug/sources (?rev=N), /w/:space/:slug/diff/:a/:b (?mode=line), /w/:space/:slug/edit, /w/:space/:slug/revert?to=N, /w/:space/:slug/settings, POST /w/:space/:slug/watch|discuss. Sign-in: /auth/login|callback|logout|me|refresh|fedcm (Network SSO, same session layer as OpenVibe.Community). Legal: /terms, /privacy, /dmca. Operations: GET /api/health, GET /api/ready (db required; Network key, Events relay, Community, Sources, Media optional → degraded), GET /release.json, GET /metrics (loopback only).

Caching: anonymous views of public published pages are public, max-age=60; signed-in views, members/private pages, editing surfaces and errors are private, no-store. A page a visitor may not read answers 404, so its existence does not leak.

Rate limits per address (besides nginx's): sign-in 60 per 15 min, API 300/min, form posts 120 per 10 min, /search 60/min, diffs 30/min, imports 20/hour. Outbound links in community spaces carry rel="nofollow ugc noopener". The threat review is docs/threat-review.md.

Per-actor limits

/api/v1 and the editing forms also limit who calls them, once req.actor is resolved and before any work (for a form, before its body is read): server/http/actor-limits.js, openvibe-sdk/limits, roadmap WS-R task 4. Counted: a person as user:usr_… (their own token or cookie, or named by a service in X-OV-Subject, or an app's on_behalf_of); a first-party service relaying a signed-out visitor by the address it forwards; a service or app acting as itself (an AI workflow's proposals) by its principal; a signed-out caller by address. A first-party service reading for itself is not counted on reads. Past a limit: 429 problem+json rate_limited with Retry-After, one [Limits] log line and wiki_rate_limited_total{limit,window}. A form and the API route that do the same thing share one budget.

Routes (API and form) Per caller, a minute / an hour
API reads WIKI_LIMITS_MINUTE / WIKI_LIMITS_HOUR (120 / 3000)
Space create (POST /spaces, /new-space) 10 / 60
Space settings and roles (PATCH /spaces/:space, PUT …/roles/:subject, /s/:space/settings) 30 / 300
Page create (POST /spaces/:space/pages, /s/:space/new) 30 / 300
Page edit and revert (POST /pages/:id/revisions, /revert, /w/…/edit, /w/…/revert) 30 / 600
Page move, visibility, delete (PATCH/DELETE /pages/:id, /w/…/settings) 30 / 300
Publish, schedule, unpublish, reviews (API and forms), proposal review 30 / 300
Citations attach 30 / 300
AI proposals (POST /proposals) 60 / 1200
Import (API and form) 5 / 20
Media attach and verify 20 / 200
Watch 60 / 600
Discussion comment (/w/…/discuss, sent to Community) 20 / 300
API search and diffs 30 / 600 each

Never limited per actor: /api/health, /api/ready, /release.json, /metrics, sign-in, and the pages people read (the per-address limits above bound them). test/actor-limits.test.js; the other tests boot with rateLimits: false, which turns off both kinds.

API /api/v1

People use their Network user JWT (Bearer, or the ov_token cookie; cross-site cookie writes are refused). Services use a Network client-credentials token for audience openvibe.wiki; every route checks one capability, and content writes need the person in X-OV-Subject (whose space role applies). Errors are RFC 9457 problem+json. The full route list is at the top of server/http/api.js.

Capability Routes
wiki.space.create POST /spaces, PATCH /spaces/:space, PUT /spaces/:space/roles/:subject
wiki.page.create POST /spaces/:space/pages, POST /spaces/:space/import, POST /pages/:id/revisions, PATCH/DELETE /pages/:id, POST /pages/:id/media[/verify]
wiki.page.read GET /pages/:id, /revisions, /revisions/:n, /diff, /revisions/:n/citations, GET /proposals/:id
wiki.revision.propose POST /proposals
wiki.revision.publish POST /pages/:id/publish, /schedule, /unpublish, POST /pages/:id/revisions/:n/review, POST /proposals/:id/review
wiki.revision.revert POST /pages/:id/revert
wiki.citation.attach POST /pages/:id/revisions/:n/citations
wiki.search.query GET /search

The ids were proposed in docs/capabilities-proposal/ (the plan's wiki.search becomes the three-segment wiki.search.query) and are released in openvibe-contracts, which owns the wiki capability manifests, the service manifest and the wiki.* event payload schemas (this repo pins v0.107.0) (docs/service-manifest-proposal.json). The proposal for wiki.revision.publish also lists the revision review route, which the released capability names since v0.32.0 (as wiki.page.create does the space import route).

Events

Through the SDK transactional outbox (openvibe-sdk/events, table wiki_event_outbox), in the same transaction as the change; the relay publishes to OpenVibe.Events when EVENTS_URL is set.

  • wiki.space.updated — created, settings, visibility, roles, renamed, deleted
  • wiki.revision.created — every revision (internal: a revision is a draft until published)
  • wiki.page.published | updated | unpublished | deleted — publication changes (public only for public, listable pages); updated also when a review changes what Search holds
  • wiki.watch.triggered — recipients who may read the page
  • wiki.index_document.upserted | deleted — search.index-document@1 documents and tombstones for OpenVibe.Search, with a monotonic index revision (createIndexSequencer)

Running it

fnm exec --using=22.22.1 npm install
cp .env.example .env
fnm exec --using=22.22.1 npm run seed     # the official "OpenVibe" space
fnm exec --using=22.22.1 npm run dev      # http://localhost:4800
fnm exec --using=22.22.1 npm test         # every test/*.test.js on PGlite (PostgreSQL in-process), no network
eval "$(node_modules/openvibe-sdk/scripts/test-services.sh up)" && npm run test:pg   # the same through PgBouncer

Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.

Production: /opt/openvibe.wiki, env /etc/openvibe/wiki.env, unit deploy/systemd/openvibe-wiki.service, database ov_wiki (DATABASE_URL through PgBouncer; migrations on DATABASE_DIRECT_URL), nginx deploy/nginx/openvibe.wiki.conf.

Depends on

  • PostgreSQL 18 and Valkey 9 (OpenVibe.Host roles/data/, ADR-035): every read and write is async through openvibe-sdk/db; Valkey holds the per-actor limit counters (optional: without VALKEY_URL they count per process).
  • openvibe-publishing v1.3.0 (async PostgreSQL stores, ingest and publication chassis), openvibe-contracts v0.107.0, openvibe-shared v4.15.2 (Frame, release, metrics, readiness, SEO helpers, legal pages), openvibe-sdk v0.65.0 (db, auth, PostgreSQL events outbox, per-actor limits, account export and deletion, testing) — pinned release tarballs.
  • OpenVibe.Network (SSO, JWKS, service principal wiki), OpenVibe.Events, OpenVibe.Community, OpenVibe.Sources, OpenVibe.Media, OpenVibe.VIP (VIP spaces and pages), OpenVibe.Search (consumer of the index events). All but the Network key are optional at runtime and degrade to explicit failure states.
  • OpenVibe.AI: generated wikis (OV_AI_INTERNAL_URL; research.gather, wiki.generate_space, wiki.generate_page). AI proposals still arrive through the proposal API.
  • Account export and deletion (ADR-033): Network grants events.subscription.manage (openvibe.events) for the two subscriptions created at boot, then, last and once the release is live, network.account.export.contribute and network.account.deletion.confirm (openvibe.network).

Account export and deletion (ADR-033)

network.account.export_requested and network.account.deleted arrive at POST /internal/events. The route is loopback-only (nginx answers 404 for /internal/, and the handler refuses a forwarded request) and signed with WIKI_EVENTS_SECRET. They are answered by server/wiki/account-data.js over openvibe-sdk/account-data, with one receipt per export and deletion in account_data_events.

  • Export: the person's spaces, pages, revisions, AI proposals, roles, watches, drafts and reviews.
  • Deleted: a user space they own is deleted the way its owner would delete it (deleteSpace as the system actor: unpublished, out of Search and the sitemap, answering 410). Their roles, watches, drafts and wiki.projects state go too.
  • Authorless: what they wrote in other spaces stays, with their id removed (created_by, proposed_by and attached_by become deleted). Revisions and citations are append-only, so only the erasure transaction may clear a revision's author (and their id in meta.authorship.authors) or a citation's attached_by (wiki.account_erasure, migration 0002_account_erasure.sql); the text and sources never change.
  • Kept: page reviews, since a person's approval is what lets reviewed text stay published.

Capabilities

Implemented here (the service manifest's capabilities, audience openvibe.wiki, one per route; routes under API): wiki.space.create, wiki.page.create, wiki.page.read, wiki.revision.propose, wiki.revision.publish, wiki.revision.revert, wiki.citation.attach and wiki.search.query.

Called elsewhere, as the service principal svc:wiki (client credentials, one token per audience; server/integrations/platform.js):

Service Grant Why
OpenVibe.Events events.event.publish the outbox relay
OpenVibe.Community community.comment.write, community.comment.moderate comment threads by reference; hiding the thread of a page that stops being public
OpenVibe.Sources sources.item.read citations that name a Sources item
OpenVibe.Media media.object.read (namespace WIKI_MEDIA_APP) attached objects and their re-verification
OpenVibe.VIP vip.resource.policy.evaluate who may read a vip space or page

Acceptance (tested in test/)

  • create → edit → publish → revise → diff → revert keeps an immutable lineage (revisions.test.js)
  • citations and infobox values stay attached to the exact revision that used them (revisions.test.js); the citation inspector shows them per revision without JavaScript and leaks nothing the article would not show (citation-inspector.test.js)
  • scheduled publication is idempotent across worker restarts (schedule.test.js)
  • a private, members-only or deleted page leaves sitemaps, feeds and the Search index (tombstone) and is served Cache-Control: private; renames 301, deletions 410 (visibility.test.js)
  • /feed.atom is a valid Atom feed with zero entries while nothing is listable (feeds.test.js)
  • a public page is useful with JavaScript disabled, and so is editing (nojs.test.js)
  • a deleted or missing Media object renders an explicit broken-asset state (integrations.test.js)
  • Media permissions survive the author/editor handoff: an editor keeps the author's attachments, cannot attach an object only the author can read, and an object Media deletes or makes private is shown as broken or withheld on the public page (media-handoff.test.js)
  • permissions are enforced; visitors without SSO read public content only; AI proposals need a person's approval (permissions.test.js)
  • only owners import into a space; invalid, oversized and colliding bundles are refused and any failure imports nothing (import.test.js)
  • every event is a valid events.event-envelope@1 and every index document a valid search.index-document@1 (visibility.test.js); the proposals validate against the contracts schemas (proposals.test.js); user text is escaped everywhere (content.test.js)
  • IndexNow: off without INDEXNOW_KEY (no key route, nothing sent); with one the key file is served at /<key>.txt as text/plain and a publish, unpublish or delete of an indexable page pings the page path and the sitemap; a draft, a noindex page and a page the gate will not index never ping (indexnow.test.js)

Security

Reporting a vulnerability: SECURITY.md. The written threat review, with code references and the gaps that remain: docs/threat-review.md.

  • Auth. People use a Network JWT (Bearer or the ov_token cookie; cross-site cookie writes are refused); services use client-credentials tokens for audience openvibe.wiki, one capability per route, and content writes need the person in X-OV-Subject, whose space role applies. Visitors without SSO read public content only; AI proposals need a person's approval.
  • Private data. Private, members-only, VIP and deleted pages leave sitemaps, feeds and Search (tombstones) and are served Cache-Control: private; a VIP refusal is never cached; Media objects the reader may not see are withheld. User text is escaped everywhere.
  • Egress. Wiki calls only its configured Network, Events, Community, Sources, Media and VIP hosts; it never fetches a URL a user chose.
  • Secrets. OV_OAUTH_CLIENT_SECRET lives in /etc/openvibe/wiki.env (0600). nginx answers /metrics with 404 and caps import bodies.

Deploy

Production deploys with sudo ovhost deploy wiki on the host (strategy git-checkout: fetch, fast-forward /opt/openvibe.wiki, install on a lockfile change, restart, wait for /api/ready). The unit is openvibe-wiki.service on 127.0.0.1:4800, the env file /etc/openvibe/wiki.env. The database is ov_wiki on the host's data role (sudo /opt/openvibe.host/roles/data/add-service.sh wiki writes its settings); the release migrates it at boot. nginx serves openvibe.wiki from deploy/nginx/openvibe.wiki.conf. Rollback: ovhost puts the previous sha back by itself when /api/ready does not answer 2xx after the restart; afterwards sudo ovhost rollback wiki --to <sha>. Nothing blocks a rollback: the schema code only adds tables and columns.

Launch rule

This repository does not make the product real on its own. openvibe.wiki kept its placeholder page on OpenVibers/OpenVibe.Sites until the following held (plan §12.12); the launch release went out on 2026-09-23:

  1. an owning runtime with health/readiness endpoints and observability — built (/api/health, /api/ready, /metrics);
  2. canonical identity/auth integration (Network subjects, a scoped service principal) — built; principal wiki and its grants are provisioned in production;
  3. server-rendered public routes useful without JavaScript — built;
  4. real persistence and end-to-end workflows — built and deployed; ovhost drill wiki restored it on the production host on 2026-09-23;
  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);
  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 (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);
  7. acceptance tests proving the advertised functionality — built (npm test).

The launch release removed openvibe.wiki from OpenVibe.Sites/sites.json, switched routing to this service and opened its Network hub entry in the same release. A placeholder is never counted as an implemented service. Still open: a person's review of the 42 seed pages (until then they stay noindex).


Part of the OpenVibe network. Built in the open by OpenVibers.

  • openvibe-contracts: v0.216.0
  • openvibe-sdk: v0.65.0
  • openvibe-shared: v4.15.2
  • openvibe-publishing: v1.3.0