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.
277 lines
14 KiB
Markdown
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)
|