From 45980cd286326a75fc052487284b174fc1f2b0f0 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sat, 26 Sep 2026 12:35:53 -0400 Subject: [PATCH] docs: add wiki-directory early plan draft --- dev/specs/wiki-directory.md | 682 ++++++++++++++++++++++++++++++++++++ 1 file changed, 682 insertions(+) create mode 100644 dev/specs/wiki-directory.md diff --git a/dev/specs/wiki-directory.md b/dev/specs/wiki-directory.md new file mode 100644 index 000000000..673f458b1 --- /dev/null +++ b/dev/specs/wiki-directory.md @@ -0,0 +1,682 @@ +# `wiki.directory` — connecting wikis to each other + +**Status:** a proposal. Nothing here is implemented. The building blocks have been chosen — +[§9](#9-decisions-taken) — but every name, path and payload shape below is still illustrative. +**Covers:** what a small shared service at `wiki.directory` could do for Wiki.js instances, what each +wiki would have to expose for it to work, and which existing standards it is built from. + +`wiki.directory` is a domain intended to make wikis discoverable to each other and to make linking +between them dependable. An administrator opts a site in, and in return links to it from other wikis +survive the site moving domain, its pages moving path, and the other wiki never having heard of it +before. + +The hard constraint is that **`wiki.directory` must stay light**: no content database, no crawler, no +relay that traffic has to pass through. Every feature below is judged against that. + +**Standards first.** Where an existing protocol does the job, it is used as written — AT Protocol's +handle pattern, WebFinger, Webmention, OpenSearch, Robust Links and Memento. What is left over and +genuinely new is small: a site identity that outlives its domain, and three page-level endpoints that +turn a page UUID into where the page is now. [§8](#8-prior-art-and-what-was-not-chosen) covers what +was considered and not chosen. + +--- + +## 1. Goals and non-goals + +**Goals** + +- **Links between wikis that do not rot.** A reference from wiki A to a page on wiki B keeps working + when B changes domain or the page is moved — and when B is gone for good, still leads somewhere. +- **Discovery.** Somebody can find wikis about a subject, and a wiki can find its neighbours. +- **Connections that are cheap to make.** Previews, backlinks, shared search — each a small exchange + between two wikis rather than a service somebody has to run. +- **Works with one participant.** Anything that is only valuable once hundreds of wikis have joined + will never get them. The first feature shipped should pay for itself on day one + ([§4.4](#44-a-shared-interwiki-map) is the candidate). Using standards helps here too: a Webmention + from a blog or an OpenSearch query from a browser is useful whether or not anybody else runs Wiki.js. + +**Non-goals** + +- **A central index of content.** The directory never holds a page, a search index or a copy of + anything a wiki publishes. +- **A relay.** Wikis talk to each other directly. The directory is asked "where is this wiki and what + is its key", and nothing else is routed through it. +- **ActivityPub-scale federation.** Following, timelines and replication of edits are a different and + much heavier project. Nothing here precludes it; nothing here needs it. +- **Non-public content.** Everything exchanged between wikis is what the public may already read on + the wiki answering. See [§3.1](#31-the-public-scoping-rule). + +--- + +## 2. Architecture: a phone book, not a hub + +The directory holds one small **signed record per participating site**. Everything else — content, +search, permissions, backlinks — stays on the wikis themselves. + +### 2.1 Identity is a key, not a domain + +A site that opts in ([§3.3](#33-nothing-happens-until-a-site-opts-in)) generates an Ed25519 keypair ([RFC 8032](https://www.rfc-editor.org/rfc/rfc8032)). +The **key is the site's identity**; the domain is merely where it currently lives. Changing domain is +publishing a new record signed by the same key, and nothing that refers to the site by its handle or +key has to change. + +**Per site, not per instance.** One Wiki.js instance hosts several sites on different hostnames, and +they are unrelated to the outside world. Each site has its own key and its own record. The private key +is a secret of the site and is stored like any other `sensitive` value — never served, masked at the +API boundary. + +### 2.2 The record + +```json +{ + "handle": "example", + "key": "ed25519:MCowBQYDK2VwAyEA…", + "baseUrl": "https://wiki.example.org", + "manifest": "https://wiki.example.org/.well-known/wiki", + "updatedAt": "2026-09-26T12:00:00.000Z", + "sig": "…" +} +``` + +- **`handle`** is the short human name, unique across the directory and used in links + (`example:Some Page`). It is a pointer to the key, not the identity itself. +- **`sig`** covers the canonical form of every other field. [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) + (JSON Canonicalization Scheme) is the obvious candidate for "canonical". +- **`updatedAt`** is what makes a replayed old record lose to a newer one. + +### 2.3 Why signed records + +Because the directory cannot forge a record, it only has to **store and serve** them, and that is +what keeps it light: + +- It can be a Cloudflare Worker over KV, static JSON files behind a CDN, or a git repository that + sites register in by pull request (the model the Public Suffix List and Homebrew taps use). Any of + these is a few kilobytes per site. +- **Anyone can mirror it**, and a mirror is as trustworthy as the original, because a wiki verifies + the signature and not the server it got the record from. +- **Wikis cache records for days.** The directory being down stalls new lookups; it does not break a + single link that has been resolved before. + +### 2.4 Handles: the AT Protocol pattern + +A site has two names, and they are verified the way AT Protocol (Bluesky) verifies a handle against an +identity: **in both directions, or not at all**. + +- **Its domain**, `wiki.example.org`. The domain names the key, through either + - a DNS TXT record at `_wikidirectory.wiki.example.org` reading `key=ed25519:…` — AT's + `_atproto.` record, or + - the key published in `https://wiki.example.org/.well-known/wiki` + ([RFC 8615](https://www.rfc-editor.org/rfc/rfc8615)) — AT's `/.well-known/atproto-did`. +- **Its short handle**, `example`, which is also the host `example.wiki.directory` — the equivalent of a + provider-hosted handle like `alice.bsky.social`. It is allocated by the directory and names the key + through the signed record. + +**A binding counts only when both sides agree.** The record, signed by the key, says the site lives at +`wiki.example.org`; the domain, independently, says its key is that key. A record alone could claim +anybody's domain, and a domain alone could point at anybody's key, so a resolver that sees only one +half treats the binding as absent. This is the AT rule, and it is what lets the directory accept +registrations without judging them. + +The well-known file is the default because the wiki can serve it itself with no DNS access; the TXT +record exists for the case where the wiki cannot answer yet — a migration in progress. + +Because the domain is verified directly, **the directory is optional for anybody who already has the +domain**: `wiki.example.org` resolves to a key with no lookup at `wiki.directory` at all. The directory +adds the short handle, and the ability to find a site again after its domain has changed. + +### 2.5 Rotation and recovery + +- **Rotation:** the old key signs a record naming the new one. The chain is kept, so a verifier that + cached the old key can follow it. +- **Recovery** from a lost key: re-prove control of the domain currently in the record, after a + waiting period during which the old key may object. This is the one path where the directory + exercises judgement, and so the one that most needs a written policy. +- An alternative worth weighing is did:plc's arrangement: a second **recovery key**, kept offline by + the administrator, that can override anything the working key signed within a fixed window + (72 hours in did:plc). Recovery then needs no judgement from the directory, at the price of an + administrator having to keep a key somewhere safe for years. + +### 2.6 The manifest + +`/.well-known/wiki` describes the site and names the endpoints that are specific to this proposal: + +```json +{ + "handle": "example", + "key": "ed25519:…", + "name": "Example Wiki", + "description": "…", + "locales": ["en"], + "license": "CC-BY-4.0", + "logo": "https://wiki.example.org/_site/…/logo", + "software": { "name": "wikijs", "version": "3.0.0" }, + "endpoints": { + "resolve": "/_api/federation/resolve", + "status": "/_api/federation/pages/status", + "describe": "/_api/federation/pages/{id}" + } +} +``` + +**Where a standard defines its own discovery, the manifest does not repeat it.** WebFinger lives at +`/.well-known/webfinger`; a Webmention endpoint is announced by a `rel="webmention"` link on the page +it is for; an OpenSearch description by `rel="search"` in the document head; a Memento TimeGate by +`rel="timegate"`. Listing them here as well would give a client two answers to disagree between, and +a non-Wiki.js client knows the standard mechanism and not this file. + +The directory caches the manifest; the listing in [§4.5](#45-discovery) is built from those copies. + +--- + +## 3. What a participating wiki exposes + +**Specific to this proposal** — named in the manifest: + +| Endpoint | Purpose | Used by | +| ---------- | ---------------------------------------------------------------- | ----------------------- | +| `resolve` | path or alias → page UUID | authoring a link, §4.1 | +| `status` | batch of UUIDs → `ok` / `moved` / `gone`, current path and title | link revalidation, §4.1 | +| `describe` | one UUID → title, description, icon, locale, last edited | previews, §4.2 | + +**Standard** — discovered the standard's own way: + +| Standard | What the wiki serves | Used by | +| -------- | -------------------- | ------- | +| [WebFinger](https://www.rfc-editor.org/rfc/rfc7033) (RFC 7033) | `/.well-known/webfinger?resource=acct:…` for users with a handle | mentions, §4.8 | +| [Webmention](https://www.w3.org/TR/webmention/) (W3C) | an endpoint receiving `source`/`target` notifications | backlinks, §4.3; mentions, §4.8 | +| [OpenSearch 1.1](https://github.com/dewitt/opensearch) | a description document and a results feed | federated search, §4.5 | +| [Memento](https://www.rfc-editor.org/rfc/rfc7089) (RFC 7089) | a TimeGate and a TimeMap per page, over the versions `/_version/` already serves | durable links, §4.1; upstream tracking, §4.6 | + +All of them are read-only except the Webmention endpoint, all are open to anonymous callers, and all +need CORS so that a reader's browser can call them directly where that is the design (previews and +embedding, §4.2; the reader's link menu, §4.1). + +### 3.1 The public scoping rule + +**Every endpoint answers as the public.** A remote wiki is an anonymous client, so what it learns +about a page is exactly what the guests group may read — through `groups.actorForPublic()`, the actor +the sitemap and the crawler half of the app shell are already built from, and +`pages.describePageForPublic`, which already produces the description `describe` needs. Search results, +TimeMaps and mementos are cut the same way, and history is its own permission: a version is served +only where the guests group holds `read:history` on the page as well as `read:pages`, the same terms +`/_version/` already applies to anybody else. Otherwise it does not exist, as far as a remote +caller can tell. + +A page the public may not read, an unpublished page and a page that does not exist are **one answer**, +for the same reason the app shell gives them one 404: the difference is not something to tell whoever +is asking. So a link to a private page on another wiki is indistinguishable from a broken one, and +that is correct. + +### 3.2 Abuse + +These endpoints are public and some are batch-shaped, so each needs a rate limit and a batch-size cap +— the postgres-backed counter `helpers/rateLimit.ts` already provides. The Webmention endpoint also +makes the wiki fetch a URL somebody else chose (to verify the source), so it gets the protections any +outbound fetch of a user-supplied URL needs: no private addresses, a size cap, a timeout, and +verification in a job rather than in the request. Each site keeps a block list of handles and of +source domains. + +### 3.3 Nothing happens until a site opts in + +Participation is **off by default and switched on per site**, from the **Discovery** card under +**Administration → General**: the "Make Discoverable in the Wiki Directory" toggle. It already exists — +the site config key `discoverable`, `false` by default (`models/sites.ts`, `api/sites.ts`, +`AdminGeneral.vue`) — and nothing reads it yet. General is behind `manage:sites`, which is the +permission registering needs anyway (§6). + +**Off**, which is how every site starts, nothing in this document exists for that site. No key is +generated and no record is published; `/.well-known/wiki` and every endpoint in the tables above +answer 404; no `rel` links or `Link` headers are added to its documents; and it makes no request to +`wiki.directory` or to any other wiki. + +**On**, the site: + +- generates its key if it has none, and publishes its record; +- serves the manifest and the endpoints above, subject to the finer controls below; +- appears in the browsable listing (§4.5), which is what the toggle's own label promises. + +**Turning it off again** publishes a signed deactivation of the record rather than simply deleting +it, so the directory, its mirrors and every wiki holding a cached copy learn the handle is withdrawn +instead of waiting for the cache to run out. It then stops serving everything above. The key is kept, +so switching back on is the same site under the same handle. Links other wikis already hold keep +working as ordinary links, because their `href` is direct (§4.1); what stops is their ability to +follow the site after a move. + +**Finer controls, in the same card**, shown only while the toggle is on. The controls and their +defaults are a proposal: + +| Control | Default when on | What turning it off withholds | +| ------- | --------------- | ----------------------------- | +| Show in the public listing | on | the §4.5 listing only — wikis that already know the handle still resolve it | +| Accept backlinks | from registered wikis | the Webmention endpoint (§4.3); the choices are off, registered wikis only, or everyone | +| Allow search from other wikis | on | the OpenSearch description and results (§4.5) | +| Allow previews and embedding | on | `describe`, and the CORS on it (§4.2) | +| Allow mentions of users | off | WebFinger (§4.8) | +| Link to other wikis | on | the outgoing half: resolving other sites' handles, fetching the interwiki map, sending Webmentions | + +`resolve` and `status` have no control of their own: answering for its own pages is what +participating *is*, so they follow the toggle itself. + +**Deliberately not in the card: whether versions are public.** That is the guests group's +`read:history` (§6) — a permission, set where permissions are set, and one this feature must not +change on the site's behalf. + +**The instance's `offline` setting overrides all of it.** An offline instance makes no outbound +request for icons today, and it makes none for this either: it neither publishes a record nor +resolves one, whatever the card says. + +--- + +## 4. Features + +### 4.1 Durable cross-wiki links + +The headline feature. Pages have stable UUIDs, and `/i/:pageId` already resolves one to wherever the +page currently lives (`helpers/pageLinks.ts`, `kind: 'pageId'`). So a durable reference is +**handle + page UUID**, resolved in two steps: + +1. **handle → current `baseUrl`**, from the directory record (cached). This absorbs a domain move. +2. **`baseUrl/i/` → current path**, by wiki B's own redirect. This absorbs a page move. + +A third case — **B is gone for good** — is what Robust Links and Memento are for, below. + +**Authors write names; the wiki stores identities.** The author writes +`[[example:Guides/Getting Started]]` — the wikilink syntax with an interwiki prefix. At save time wiki A +calls B's `resolve` endpoint for that path and keeps the UUID alongside the name: the name so the +source stays readable, the UUID as the real target. + +#### The stored render: a Robust Link + +A render is produced once, in the editor's browser, so whatever the link needs later has to be in the +markup from the start. [Robust Links](https://mementoweb.org/robustlinks/spec/) is the existing +convention for exactly that — an ordinary `` that also records *when* it was made and *where a +copy from that time is*, so that a reader can get to what the author cited after the live page has +changed or gone: + +```html +Getting started guide +``` + +- **`href`** is B's direct URL as `resolve` reported it at save time. A click goes straight to B and + never touches the directory. +- **`data-versiondate`** is the Robust Links date of linking. On its own it is enough to ask any + Memento TimeGate — B's own or a web archive's — for the version nearest to it, years later. +- **`data-versionurl`** is a memento of the page as it was when linked: B's own `/_version/` URL + where the public may read B's history (below), a web archive snapshot where one was made, or + absent. +- **`data-wiki-ref`** is the one attribute this proposal adds: the durable reference, from which the + current location can always be worked out again. + +Robust Links' rule is that when `href` is the live page, the version attributes are what is recorded, +and `data-originalurl` is used only when `href` points at the memento. The live page is the right +`href` for a wiki, which is linking to a living document rather than citing a fixed text. + +The sanitiser in `models/rendering.ts` has to allow these four attributes on ``, if it does not +already. + +#### Keeping the `href` right + +`pageLinks` gains a `remote` kind, so that the link table — which today deliberately records nothing +leaving the wiki — tracks these. A scheduled task posts batches of UUIDs to each linked site's +`status` endpoint: + +- `moved` updates the stored `href`, or — more conservatively — reports "N links point to moved + pages" for an editor to accept (open question, [§10](#10-open-questions)). +- `gone` flags the link as broken on the page and in a link report. `gone` is more useful than a bare + 404: B can answer with a tombstone carrying the title the page had and, where it was merged, where it + went. +- A site whose handle has been deactivated, or that has not answered for long enough, is marked as + **gone as a whole**, and its links switch to their archived form (below). + +#### What a reader sees + +A link carrying `data-wiki-ref` gets a small menu in the reader's browser, in the manner of the +reference `robustlinks.js`, but drawn by the app: + +- **Current page** — `href`, or `https://example.wiki.directory/i/` when revalidation has said the + `href` is stale and it has not been rewritten yet. +- **As it was on 26 September 2026** — `data-versionurl` when present, otherwise a TimeGate asked for + `data-versiondate`. +- **Copy durable link** — the redirector form, which is what a link pasted into an email should be, + since there is no wiki behind that email to revalidate it. + +#### Memento: the version as linked + +[Memento](https://www.rfc-editor.org/rfc/rfc7089) (RFC 7089) is the HTTP-level half of Robust Links: +a **TimeGate** takes an original URL and an `Accept-Datetime` and redirects to the **memento** — the +version current at that time — and a **TimeMap** lists every memento there is. A wiki with page +history is a natural Memento origin server; MediaWiki has had an extension doing exactly this for a +decade. + +- **B serves its own mementos.** Every version already has a URL of its own, `/_version/`, which + is the memento. What Memento adds around it: each page document carries + `Link: <…/timegate>; rel="timegate", <…/timemap>; rel="timemap"`; the TimeGate maps a datetime to the + `pageHistory` version current then and redirects to its `/_version/`; and that document is served + with `Memento-Datetime` and a `rel="original"` link back to the page. This is what `data-versionurl` + points at, and it means "as it was when linked" works with no third party involved — where the + public may read the page's history (§3.1, §6). +- **Web archives cover B being gone.** When B no longer answers, the same `href` and + `data-versiondate` go to a public TimeGate instead — the Internet Archive's, or an aggregator such as + Memento Time Travel that asks several archives at once. That only finds something if an archive + captured the page, which is why A may *ask* for a capture at link time (open question, §10). +- **The TimeMap doubles as the history endpoint** for upstream tracking ([§4.6](#46-reusing-content-across-wikis)), + so there is no separate `history` endpoint to specify. + +A reader's browser cannot set `Accept-Datetime` on a plain navigation, so the "as it was" item asks the +TimeGate with `fetch` and then navigates to the `Location` it answers with. B's TimeGate therefore +needs CORS that allows the `Accept-Datetime` header and exposes `Location`. + +#### The redirector + +`https://example.wiki.directory/i/` costs almost nothing to run: wildcard DNS on +`*.wiki.directory` and a Worker that reads the handle's record and answers 302 to +`/i/`. Its job is the **current** location and nothing more. It cannot do the archival +fallback — it knows a UUID, not what URL the page had or when it was linked — which is precisely why +that information lives in the Robust Links attributes of the render rather than anywhere central. + +### 4.2 Previews and embedding + +Once A can address `example/`, it can ask B to `describe` it: + +- **Hover cards** on cross-wiki links — title, summary, icon, last edited — in the way Wikipedia's + page previews work. Fetched in the reader's browser, cached briefly, never stored by A. +- **Embedding a section** as a block: ``, a read-only + excerpt with an attribution line. Fetched by the reader's browser over CORS, so A holds nothing. + The manifest's `license` decides whether the block agrees to render at all, and the excerpt goes + through the same sanitiser rules as any render. + +### 4.3 Backlinks across wikis: Webmention + +"Referenced by 3 other wikis", on B's page. This is [Webmention](https://www.w3.org/TR/webmention/) +as written, with a signature layered on top for wikis in the directory. + +**The standard part:** + +1. B announces its endpoint on every page document: `Link: ; + rel="webmention"`. The app shell (`renderAppShell`) is where that header goes. +2. When A saves a page that links to a page on B, A fetches the target, discovers the endpoint, and + POSTs `source=&target=` as `application/x-www-form-urlencoded`. +3. B answers `202 Accepted` and verifies in a job: it fetches `source` and checks that it really links + to `target`. The crawler half of A's app shell already puts the page's render in the document, so + the link is there to find with no JavaScript run — and fetching it anonymously is also what proves + the source page is public. +4. When A removes the link, or deletes the page, it sends the same notification again. B re-verifies, + finds the link gone or the page answering 404/410, and drops the backlink. + +**The addition:** a Webmention from a wiki in the directory is signed with that site's key using +[HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421) (RFC 9421), with the handle as the +key id. B checks it against the directory record. + +- **Signed** Webmentions from registered sites are accepted once verified. +- **Unsigned** ones — any blog, any IndieWeb site, any other wiki engine that speaks Webmention — are a + per-site setting: refused, accepted, or held for moderation. Holding needs a moderation queue that + does not exist yet. + +The directory sees none of this. Because the transport is standard, a Wiki.js site gets backlinks from +the whole Webmention-speaking web on day one, whether or not a single other wiki has joined. + +The same endpoint carries **cross-wiki mentions** (§4.8): a mention is a Webmention whose target is the +mentioned user's profile URL. + +### 4.4 A shared interwiki map + +The cheapest feature and the one that is useful with a single participant. MediaWiki has had +interwiki prefixes for twenty years, but each installation keeps its own table by hand. +`wiki.directory/interwiki.json` would be one shared map: + +- **Registered sites** resolve durably, through §4.1. +- **Everything else** is a URL template — `wikipedia:` → `https://en.wikipedia.org/wiki/$1`, + `rfc:` → `https://www.rfc-editor.org/rfc/rfc$1`, `arch:`, `mdn:` and so on — with no durability + and no claims beyond "this is where that prefix points". Wikimedia's global interwiki map on Meta is + the obvious seed, rather than starting empty. + +Each site can add prefixes of its own on top, and override shared ones. The map is static JSON, so it +is served from a CDN and cached by every wiki. + +### 4.5 Discovery + +- **A browsable listing** at `wiki.directory`, built from the cached manifests of discoverable sites + that have not turned off **Show in the public listing** (§3.3) — a site may want durable links + without appearing in a directory. Topics, languages, license, size, recent activity. Static, regenerated + on a schedule. +- **Federated search over OpenSearch, run by the wiki and not the directory.** Described below. +- **Collections.** A signed list of handles — "every team wiki of one organisation". Members get a shared + navigation strip, prefix resolution scoped to the collection, and search across the collection by + default. This suits organisations that run many small wikis more than any single feature above. + +#### OpenSearch + +Each site publishes an [OpenSearch 1.1](https://github.com/dewitt/opensearch) description document +and announces it in the document head, which the app shell writes: + +```html + +``` + +Under `/_site`, the per-site resource controller, rather than at the root: a root file name has to be +added to `RESERVED_ROOT_FILES` and taken away from pages, and nothing about OpenSearch needs the root. + +The description declares URL templates by result type: + +- **`text/html`** → the wiki's own search page. This alone makes browsers offer the wiki as a search + engine, which is worth having independently of anything else here. +- **`application/atom+xml`** → results as an Atom feed with the `opensearch:` elements + (`totalResults`, `startIndex`, `itemsPerPage`). This is the interoperable form, and what A reads + from a peer that is not Wiki.js. +- **`application/json`** → the same results with what a Wiki.js peer can show and Atom has no place + for: locale, icon, tags, the page UUID. + +Federated search is then: an administrator picks peer sites; the search page offers "also search in +…"; A fetches each peer's description once, fills in the template, and merges the results. There is +no central index, and the directory's only part was telling A where the peers are — and for a peer +named by its domain, not even that. + +### 4.6 Reusing content across wikis + +- **Fork with an upstream.** Import a page from B into A and record `derivedFrom: example/` with the + memento it was copied from. A later look at B's TimeMap shows whether newer mementos exist — + "upstream has changed since this was copied" — and the two mementos give the diff. Shared + procedures, style guides and templates are the use case. The copy's attribution line is itself a + link to B, so B learns about the fork through an ordinary Webmention. +- **Translations across wikis.** A page's translation group (`localeGroupId`) currently links pages of + one site. A group with a remote member would let a community run a French wiki that is formally a + translation of an English one, and send the locale switcher across. + +### 4.7 A block registry + +`.wkblock` ([wkblock.md](./wkblock.md)) already makes a block a single, checksummed file installed by +upload. `wiki.directory` is the natural catalogue for them: + +- Authors sign packages with a key of their own, registered the same way a site's is. +- **Administration → Content Blocks** gains a browse view that lists community blocks and installs one + by URL, with the signature checked before the importer's own checks run. +- The directory holds metadata, the checksum and the signature; the package itself can stay on the + author's release page. + +This answers the first of `wkblock.md`'s known gaps — that a signature proves nothing useful without +a notion of publishers — by providing the publishers. The same shape fits the YAML-only module types +(analytics and comment providers) and icon sets, all of which are data rather than server code. + +### 4.8 Identity across wikis + +The heaviest idea, and the one to leave until the rest has proven itself. + +#### Mentions: WebFinger, then Webmention + +`@alice@example` in a page on A names Alice on B. Handles are already unique per wiki (`users.handle`), +so the pair is unique across the directory. + +1. **`example` → `wiki.example.org`**, through the directory. A mention written with the domain, + `@alice@wiki.example.org`, skips this step — the same form the fediverse uses. +2. **WebFinger** ([RFC 7033](https://www.rfc-editor.org/rfc/rfc7033)): + `GET https://wiki.example.org/.well-known/webfinger?resource=acct:alice@wiki.example.org` answers a JRD + whose `http://webfinger.net/rel/profile-page` link is Alice's profile, `/_user/`. Only an + account with a handle has an answer; one without is not mentionable, exactly as it is locally. +3. **Webmention** to B, with `target` set to that profile URL. B verifies it as any other (§4.3) and + notifies Alice. + +**A caveat from verification:** Webmention requires the source document to contain the link, and the +crawler half of the app shell carries the page render only. Comments are rendered in the browser at +display time, so a mention inside a comment is not in any document B could fetch. Mentions are +therefore limited to page content, unless the crawler document grows the page's comments. + +#### "Sign in with your wiki" + +Each site can act as an OIDC or IndieAuth provider for its own users, with WebFinger and the +directory supplying discovery. Alice comments on C with her identity from B and no account on C. + +The trust questions here — what C should believe about an identity B vouches for, and what happens +when B is hostile — are real and are not answered by anything in this document. + +--- + +## 5. What already exists + +| Piece | Where | Relevant to | +| ----- | ----- | ----------- | +| Page UUIDs and `/i/:pageId` | `db/schema.ts` `pages.id`, `frontend/src/router/routes.js` | §4.1 | +| Link table with kinds | `db/schema.ts` `pageLinks`, `helpers/pageLinks.ts` | §4.1 revalidation | +| Wikilink syntax | `frontend/src/renderers/modules/markdown-it-wikilinks.js` | §4.1, §4.4 prefixes | +| Page history | `pageHistory` | §4.1 mementos, §4.6 | +| A URL per version | `/_version/:versionId`, `GET /sites/:siteId/versions/:versionId` | §4.1 mementos | +| The app shell: head, headers, crawler body | `helpers/appShell.ts` | §4.1 `Link` headers, §4.3, §4.5 | +| The public as an actor | `groups.actorForPublic()` | §3.1 | +| A page described for the public | `pages.describePageForPublic` | `describe`, §4.2 | +| Per-site resource routes | `controllers/site.ts` (`/_site`) | §4.5 OpenSearch description | +| Sitemap with `hreflang` | `controllers/rootFiles.ts` | §4.5 | +| Translation groups | `pages.localeGroupId` | §4.6 | +| Unsigned block packages | `helpers/wkblock.ts`, `blocks/package.mjs` | §4.7 | +| User handles and profile pages | `users.handle`, `/_user/:userId` | §4.8 WebFinger | +| Postgres-backed rate limiter | `helpers/rateLimit.ts`, `models/rateLimits.ts` | §3.2 | +| The Discovery card and `discoverable` | `AdminGeneral.vue`, `models/sites.ts`, `api/sites.ts` | §3.3 | + +None of WebFinger, Webmention, OpenSearch or Memento is implemented anywhere today. + +--- + +## 6. Prerequisites in the wiki itself + +**A page UUID has to be a permanent identity**, and today it is not quite one: + +- **A storage import mints new ids.** `pageMeta` in `helpers/storageFiles.ts` writes no `id` into a + page file's front matter, so a site rebuilt from its disk or git target comes back with every page + under a new UUID, and every durable link pointing at it breaks. Writing the id, and honouring it on + import when it does not collide, closes that. +- **A restore into a different site re-keys everything** — by design, per + [wkbackup.md §4](./wkbackup.md#4-identity-derive-uuids-do-not-map-them), so a copy can sit beside + the original. That is right for a copy and wrong for a site that is *moving* to a new instance. A + move should either restore into the same site id, or the re-keying should be recorded so that the + old UUIDs keep redirecting. + +**Mementos are only as public as history is.** Every version already has a URL of its own — +`/_version/` — so nothing new is needed to address one. But reading it takes `read:history` on the +page as well as the right to read the page, and nothing is granted by default, so on most wikis the +public holds no `read:history` anywhere. There, the "as it was when linked" link would answer 404 to +every reader on another wiki, and `data-versionurl` should not be written at all. A site that wants +the version as linked to work grants guests `read:history` where it wants that — this proposal must +not loosen it on the site's behalf. `data-versiondate` still leads to a web archive either way. + +What Memento adds on top of the existing route is headers — `Memento-Datetime` and `rel="original"` +on the `/_version/` document the app shell serves — plus the TimeGate and TimeMap. + +**The site's key needs a home**: a site-level secret, masked like other `sensitive` values, included in +a backup (or a restored site cannot prove it is the same site) and excluded from anything served. + +**Naming.** The Webmention endpoint is not called an "inbox" anywhere in the product: `/_inbox` is +already the user's own messages and review queue. + +**Registering is the Discovery card on Administration → General** (§3.3), and `manage:sites` — the +permission General already requires — is the one that covers what it touches. No new global permission is proposed here; if one turns out to be needed, +that is the maintainer's call. + +--- + +## 7. Suggested order + +1. **Identity:** keys per site, signed records, the two-way handle check, `/.well-known/wiki`, the + directory as a store of records — all switched by the existing `discoverable` toggle, with the + finer controls of §3.3 added as each feature they govern arrives. Everything else depends on it. +2. **The interwiki map** (§4.4) and the **OpenSearch description** (§4.5). Both small, and both + valuable before anyone else has joined. +3. **Durable links** (§4.1) as Robust Links, with revalidation and the redirector, then **previews** + (§4.2). The headline feature, and the page-identity prerequisites in §6 come with it. +4. **Memento** on B (headers on `/_version/`, TimeGate, TimeMap) and **Webmention** (§4.3), in + either order. + Both are standard on the wire, so both are useful with peers that are not Wiki.js. +5. The **block registry** (§4.7), then everything else as demand shows up. + +At the end of step 4 the directory is still a key-value store of a few kilobytes per site, a static +JSON map and a static listing — which is the constraint this document started from. + +--- + +## 8. Prior art, and what was not chosen + +| Considered | What it is | Why not | +| ---------- | ---------- | ------- | +| **did:webvh** | A DID method: a hash-chained, signed log hosted on the site itself, with a self-certifying id that survives a domain move | Closest fit for identity, and needs no registry. Not chosen: a log, hash chain, multikey encoding and Data Integrity proofs are a good deal more machinery than one signed record, and it still cannot find a site again after a move — a lookup from its id to the current domain is needed either way, which is the directory. | +| **did:plc** | Bluesky's DID method: a central directory of signed operation logs | The design in §2 is essentially this, minus the DID document format. Its recovery-key window is kept as an option in §2.5. | +| **did:web** | A DID resolved from `/.well-known/did.json` on a domain | The identity *is* the domain, so it cannot move — the problem this proposal exists to solve. | +| **DOI / Handle System, ARK, PURL, w3id.org** | Persistent identifier registries that redirect | Durable per *resource*: every page that moves needs its registry entry changed, which is the heavy central database this avoids. Here the directory knows sites, and each wiki answers for its own pages. | +| **ActivityPub** (e.g. Ibis) | Server-to-server federation; Ibis mirrors articles and edits across instances | Replication, following and timelines are the non-goal in §1. | +| **Federated Wiki** | Ward Cunningham's wiki, where pages are forked between sites and search covers a "neighbourhood" | The conceptual ancestor of §4.5–4.6, with its own protocol and a small community. Inspiration, not a dependency. | +| **Linked Data Notifications** | A W3C inbox protocol | Webmention does the same job with less, and far more of the web already speaks it. | +| **NodeInfo** | The fediverse's `/.well-known/nodeinfo` software and usage metadata | Overlaps the manifest's `software` block. Could be served alongside it later; nothing depends on it. | + +--- + +## 9. Decisions taken + +Taken on 2026-09-26. + +- **Participation is opt-in, per site**, from the Discovery card on **Administration → General** — + the existing "Make Discoverable in the Wiki Directory" toggle, with finer controls in the same card + (§3.3). Off, a site publishes nothing and contacts nobody. +- **Identity and handles are the design in §2**: a per-site Ed25519 key, a signed record, and handles + verified in both directions after the AT Protocol pattern. did:webvh was considered and not adopted + (§8). +- **WebFinger** resolves users for mentions (§4.8). +- **Webmention** is the transport for backlinks and mentions, with RFC 9421 signatures added for + registered sites (§4.3). +- **OpenSearch** describes each site's search, for browsers and for federated search (§4.5). +- **Robust Links** is the form of a cross-wiki link in the stored render, and **Memento** (RFC 7089) + is how the version as linked is reached — from B's own history while B exists, from web archives + after (§4.1). This replaces the "direct vs redirector" choice this document used to leave open: the + `href` is direct, and the redirector is the current-location fallback and the copyable form. + +--- + +## 10. Open questions + +- **Handle policy.** First come, first served? Reserved names? What happens to a handle whose domain + has lapsed and been bought by somebody else — who wins, the key or the domain? +- **Governance.** Recovery (§2.5) and disputes need somebody to decide them. Who, under what written + rules, and how does a site appeal? Or does a did:plc-style recovery key remove the need? +- **Rewriting stored renders.** When revalidation finds a moved page, should the `href` in the render + be rewritten — given that a render is otherwise only ever produced by an editor's browser — or + should it only be reported for an editor to accept? +- **Capturing at link time.** Should A ask a web archive to capture B's page when the link is saved, + so that `data-versionurl` has something outside B to point at? It makes the gone-for-good case work, + and it also tells a third party what the wiki links to. +- **Privacy of the redirector.** Should it log anything at all? +- **Unsigned Webmentions.** The card offers the choice (§3.3). Should "everyone" hold them for + moderation rather than accept them — and holding needs a moderation queue that does not exist yet. +- **WebFinger exposure.** The site decides whether users can be mentioned at all (§3.3). Once it + has, answering for every account with a handle confirms to anybody that the account exists. Is + having a handle consent enough, or is being findable a separate per-user choice? +- **Non-Wiki.js wikis.** The three specific endpoints in §3 are small enough for a MediaWiki extension + or a DokuWiki plugin to implement, and everything else is already standard. Is that a goal? If so, + the protocol wants its own document separate from any one implementation — as `wkblock.md` is + separate from its writer and reader. +- **Licensing.** Embedding (§4.2) and forking (§4.6) copy content between sites. Is the manifest's + `license` field enough, or does a page need a license of its own? +- **Linking out without joining.** Every outgoing feature sits under the toggle, so a site that + wants `[[other:Page]]` links to resolve durably has to publish a record of its own first. Is a + consume-only mode worth having — resolving others' handles with nothing published — or is joining + a fair price for using the directory? +- **The defaults in the card** (§3.3) are a first guess, in particular "Show in the public listing" + being on: the toggle's label says *discoverable*, which argues for it.