Files
member-console/docs/fedwiki-setup.md
T
cgalo5758 6dbce6140f Type the ConfigSpec seam and move the connect target to core
Register bool and duration ConfigSpec keys from the Default's type, move
fedwiki's four sync knobs and discourse's two into their integrations'
ConfigSpecs, and replace core's read of fedwiki-custom-domain-target
with a core domains-connect-target key resolved once and threaded
through server and worker config.

Generate init's optional-integration scaffold sections from each
registered ConfigSpec instead of the hand-maintained list, and reword
the Temporal boot warning generically.

Archives the integration-config-parity change; status bookkeeping and
the verify-skill doc follow with the test-stack commit.
2026-08-01 04:13:48 -05:00

277 lines
14 KiB
Markdown

---
title: "FedWiki Integration"
audience: [developer, admin]
summary: "How member-console provisions FedWiki sites through the FarmManager API: security stack, endpoints, configuration, and local dev/test setup."
---
# FedWiki Integration
This document describes how the member-console integrates with FedWiki for wiki site provisioning and management.
## Architecture Overview
The member-console manages FedWiki sites through the **FarmManager API**, a plugin that runs on a designated admin wiki site within a FedWiki farm. The security layer uses a composable architecture that separates authentication from authorization.
### Components
| Component | Package | Role |
|-----------|---------|------|
| Security layer | `wiki-security-composable` | Composable security plugin that separates authentication from authorization. Allows mixing auth providers with authorization enhancers. |
| Authentication | `wiki-security-social` | OAuth2/OIDC authentication via Keycloak (better-auth based). Configured per wiki domain in `config.json` under `wikiDomains` using `oauth2_discoveryUrl`. Supersedes `wiki-security-passportjs`. |
| Authorization | `wiki-plugin-useraccesstokens` | Adds API token (Bearer) authentication. Tokens are stored per-site in `status/user-access-tokens.json`. |
| Farm management | `wiki-plugin-farmmanager` | REST API for programmatic farm management (create, list, delete sites). Requires admin authentication. |
### How FarmManager Works
FarmManager is a **site-level plugin with farm-level scope**. It loads per-site (standard FedWiki plugin architecture), but manages all sites in the farm by resolving the parent farm directory from the current site's data path.
In practice, one site is designated as the **admin site** (e.g., `admin.localtest.me`). All farm management API calls go through this site. The plugin uses `app.securityhandler.isAdmin(req)` middleware to restrict access to farm admins.
## FarmManager API
All endpoints are served from the admin site and require admin authentication via Bearer token.
Base URL: `http://admin.localtest.me` (local dev)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/plugin/farmmanager/sites` | List all wiki sites in the farm |
| `POST` | `/plugin/farmmanager/sites` | Create a new wiki site |
| `GET` | `/plugin/farmmanager/sites/:domain` | Get details for a single site |
| `PATCH` | `/plugin/farmmanager/sites/:domain` | Partially update a site's properties |
| `DELETE` | `/plugin/farmmanager/sites/:domain` | Soft-delete (deactivate) a site |
| `DELETE` | `/plugin/farmmanager/sites/:domain?hard=true` | Permanently delete a site |
### Authentication
The member-console authenticates to the FarmManager API using a **User Access Token** (prefix: `fwuat-`), sent as a Bearer token in the `Authorization` header.
Tokens are generated by `wiki-plugin-useraccesstokens` and stored as bcrypt hashes in the admin site's `status/user-access-tokens.json`. Each token has:
- A `name` (unique per site)
- A `user` (the owner's OAuth2 identity)
- A `tokenHash` (bcrypt hash of the token)
- A `displayHint` (last 4 characters, for identification)
- Optional `scopes` (`site:read`, `site:write`)
## Member-Console Configuration
Relevant settings in `mc-config.yaml`:
```yaml
# URL of the admin wiki site (where FarmManager plugin is loaded)
fedwiki-farm-api-url: "http://admin.localtest.me"
# Domains where wiki sites can be created
fedwiki-allowed-domains:
- "localtest.me"
# URL scheme for generated site links
fedwiki-site-scheme: "http"
# User Access Token for FarmManager API authentication
fedwiki-admin-token: "fwuat-REPLACE-WITH-YOUR-USER-ACCESS-TOKEN"
```
The `FarmManagerClient` in `internal/integrations/fedwiki/workflows/farmmanager_client.go` constructs API URLs as `baseURL + path` (e.g., `http://admin.localtest.me/plugin/farmmanager/sites`).
## Local Development Setup
### FedWiki Farm Structure
The FedWiki farm runs in Docker (see `test/compose.yaml`) on port 80 with these key directories:
```
test/data/fedwiki/ # Runtime data (mounted as /home/node/.wiki)
config.json # Farm configuration (OAuth2, wikiDomains, admin identity)
admin.localtest.me/ # Admin site (FarmManager API host)
status/
owner.json # Site owner identity (OAuth2 subject)
user-access-tokens.json # API tokens for Bearer auth
localtest.me/ # Root farm site
<subdomain>.localtest.me/ # User-created wiki sites
```
### Seed Data
Seed data lives in `test/seed/fedwiki/` and is copied to `test/data/fedwiki/` on `docker compose up` via the `fedwiki-init` container (`cp -rn`, no-clobber).
The seed includes:
- `admin.localtest.me/status/owner.json` — Admin site owner (alice, pinned ID: `a0000001-0000-4000-a000-000000000001`)
- `admin.localtest.me/status/user-access-tokens.json` — Pre-configured API token for member-console
### config.json
The `config.json` is **not seeded** — it is runtime-generated by FedWiki on first boot, or must be manually created. It contains:
- `admin` — Farm admin identity (OAuth2 subject). Must match a Keycloak user.
- `farm: true` — Enables farm mode.
- `wikiDomains` — Per-domain OAuth2 configuration (client ID/secret, Keycloak endpoints).
- `security_type`, `auth_provider`, `authz_enhancers` — Composable security stack.
If `config.json` is lost, FedWiki will generate a default one on startup, but you'll need to re-configure the OAuth2 wikiDomains and admin identity.
### Keycloak User IDs
Test user IDs are **pinned** in `test/seed/keycloak/seed-keycloak.sh` so that FedWiki seed data (owner.json, config.json) can reference them deterministically:
| User | ID | Role |
|------|----|------|
| alice | `a0000001-0000-4000-a000-000000000001` | Operator, FedWiki farm admin |
| bob | `b0000002-0000-4000-a000-000000000002` | Member |
| carlos | `c0000003-0000-4000-a000-000000000003` | Member |
| diana | `d0000004-0000-4000-a000-000000000004` | Member |
The Keycloak bootstrap `admin` user ID is assigned by Keycloak and is not pinned.
### Common Issues
**"Requested Wiki Does Not Exist" from FarmManager API**
- The admin site directory doesn't exist in `test/data/fedwiki/`. Ensure `admin.localtest.me/` exists with at least `status/owner.json`.
- Fix: Remove `test/data/fedwiki/` and restart docker compose to re-seed.
**EACCES permission denied**
- The FedWiki container runs as `node`. If the data directory is owned by root (e.g., after Docker recreates it), FedWiki can't write to it.
- Fix: `chmod -R 777 test/data/fedwiki/` or ensure your user UID matches the container's `node` user.
**Owner mismatch after Keycloak re-seed**
- If Keycloak user IDs change, the OAuth2 `id` in FedWiki's `owner.json` and `config.json` won't match. User IDs are now pinned to prevent this.
## Custom Domains
Members on an entitled plan can point domains they own at FedWiki sites.
Domain names are managed by the console's **domains registry**
(`domains-registry` capability): every servable name is a **placement**
inside a **claim** — an operator shared-domain root, a member claim carved
from it (hosted sites), or a member-owned **external claim** proven once by
DNS verification. Operator enablement takes two steps:
1. **Author the entitlement rule.** Add a `boolean` rule with resource key
`external_domain_claims` to the entitlement set of any plan that should
include custom domains (Operator → Entitlement Sets → Rules). The
affordance appears only for workspaces whose pool holds the granted
entitlement.
2. **Set the connect target.** Configure the core `domains-connect-target`
key (environment, config file, or CLI flag) to the DNS name or IP members
should point their domains at. While unset, the custom-domain flow stays
hidden even for entitled workspaces. The capability is core's — the same
key gates external claims everywhere, not just FedWiki's create flow.
The member flow is **verify once, place freely**: a domain is verified at
the account level (the member "Domains" page, or inline from the Create
Site dialog), and once its claim is active the member can create sites at
that name — or any name inside it (`wiki.example.org`,
`docs.wiki.example.org`, …) — instantly, with no further verification.
Verification shows a records table to publish: a TXT challenge at
`_member-console-challenge.<domain>` proving control, a CNAME/A record
pointing at the connect target, and an **optional wildcard CNAME**
(`*.<domain>` → target) so nested names need no per-site DNS work. A
Temporal workflow polls for the TXT record (24-hour window by default) and
activates the claim when it appears. Verification no longer auto-creates a site — the
sites list offers a one-click create once the domain verifies. Unclaimed
names are never served.
While a claim is pending:
- Each poll probes **both** records and the status view shows a per-record
result — not found yet, found-but-mismatched (with the values we
observed), or found. Only the TXT challenge gates verification; the
CNAME/A probe is diagnostic so members can fix routing before the site
exists. Record names and values are copyable in one click.
- **Check now** re-probes immediately (and resets the poll backoff) instead
of waiting out the interval, which grows to 15 minutes.
- **Cancel verification** frees the domain immediately (claim `canceled`)
and stops the polling workflow — a mistyped domain no longer squats the
name for the full window.
- The instructions stay reachable from the sites list's pending banner and
from the Domains page; at most 5 claims may be pending per workspace.
Walking this flow against a real zone (and the wildcard-record trap that makes a
correct setup look broken) is documented in
[`docs/testing.md`](testing.md) under "Live DNS verification".
### Claim policy and the abandonment ledger
Because a live claim holds its whole subtree against every other workspace,
abandoning one is metered. A claim that is canceled or expires **without the
system ever having seen a challenge-prefixed TXT record for it** is recorded
as an abandonment; once a workspace reaches its budget for a domain scope
(the name's last two labels and everything beneath them, so cycling sibling
names counts the same as cycling one), further claims there are refused
until enough entries age out of the window. The refusal tells the member
about their own history and when they may retry — it reveals nothing about
anyone else.
A member who published the challenge record is exempt: proving zone control
means cancel-and-retry stays free, and verifying a name clears the entries
recorded at it and beneath it. Cancellations the console itself performs
(for example when a verification workflow cannot start) are never counted,
against either budget, and neither are the claims adopted for custom domains
that predate the registry.
Operators can inspect live claims across all workspaces at
**Operator → Domains**, see each claim's age, holder, placements and whether
evidence was observed, force-release a squatted name, and read the effective
policy values. All of the policy is per-deployment configuration:
| Key | Default | Meaning |
|---|---|---|
| `domains-claim-window` | `24h` | Time to publish the TXT challenge |
| `domains-pending-cap` | `5` | Concurrent pending claims per workspace |
| `domains-abandon-budget` | `3` | Abandonments per scope before refusal (`0` disables the ledger) |
| `domains-abandon-window` | `168h` | Rolling window abandonments are counted over |
| `domains-scope-labels` | `2` | Labels forming a scope root |
| `domains-initiation-budget` | `10` | New claims per workspace per 24h |
| `domains-expiry-sweep-interval` | `15m` | How often expired pending claims are swept |
A pending claim is normally expired by its own verification workflow at the
deadline. The expiry sweep is the backstop for claims whose workflow is gone —
a worker crash, a terminated execution, an incompatible deploy — so a name is
never held past its window by an accident of the console's own. It runs once at
every boot regardless of whether Temporal is configured, and on the interval
above wherever it is.
The **Domains page** (`/domains`) lists the workspace's claims with
per-claim status and placement counts, hosts the DNS instructions, and
releases claims that no longer hold any site. Name policy (reserved,
blocked, and premium labels under shared domains, plus a built-in
single-letter guard) is operator data in `domains.name_rules`; members see
policy refusals as a plain "unavailable".
### Serving authorization (`/domains/ask`)
The console answers the on-demand-TLS ask contract at
`GET /domains/ask?domain=<fqdn>`: HTTP 200 authorizes certificate issuance
for exactly that name (a servable placement exists in the registry), any
other answer refuses it. Point Caddy at it:
```caddyfile
{
on_demand_tls {
ask http://member-console:8080/domains/ask
}
}
```
The endpoint is unauthenticated (TLS proxies send no credentials) — expose
it to the proxy's network only. This replaces filesystem-based ask
answerers (a domain is servable because the console says so, not because a
directory exists). For deployments migrating off a legacy answerer, set
`domains-ask-fallback-url`: names unknown to the registry are then
forwarded to the legacy endpoint (strangler pattern) — a name the registry
knows but has archived is refused locally, never resurrected by the
fallback.
**JSON API behavior:** `POST /api/fedwiki/sites` with `isCustomDomain:
true` enforces the entitlement gate (403 when not entitled) and then
branches on the domain's claim state: unclaimed → 202 with the claim's DNS
record details; a claim of the caller's own still pending → 400
"verification in progress"; a name inside the caller's active claim → the
normal instant-create response. Conflicts with names held by others are
reported only as "unavailable".
## Plugin Source Code
- [wiki-security-composable](https://git.coopcloud.tech/wiki-cafe/wiki-security-composable)
- [wiki-plugin-farmmanager](https://git.coopcloud.tech/wiki-cafe/wiki-plugin-farmmanager)
- [wiki-plugin-useraccesstokens](https://git.coopcloud.tech/wiki-cafe/wiki-plugin-useraccesstokens)