Theme
Master server API
The WFX.re master server is master-server/server.js — Node.js, no dependencies. It serves the server list, network statistics, WFX.re accounts, the launcher linking flow, the Portal, published server images and the wfx.re landing page. The project's own (French) reference is docs/MASTER_API.md.
| Setting | Default | Meaning |
|---|---|---|
PORT | 7777 | Listening port |
PUBLIC_URL | the request's Host | Base for public URLs: iconUrl, bannerUrl. https: also turns on Secure cookies. |
LINK_URL | PUBLIC_URL | Base of the account-linking page given to the launcher (<LINK_URL>/link?code=…). On WFX.re: https://auth.wfx.re, while PUBLIC_URL is https://servers.wfx.re |
DEMO | on | DEMO=0 disables the four demo servers shown in development |
CLIENT_VERSION | 0.1.0 | Returned by /version |
DOCS_DIST | ../docs-site/docs/.vitepress/dist | Folder served under /docs/ |
Data lives in master-server/data/: accounts.json (users with scrypt + salt; sessions keyed by sha256(token), never the token itself), portal.json (registered servers; sha256(sv_key) plus its last 4 characters), media/<serverId>/icon.png and banner.(png|jpg).
All JSON responses carry Cache-Control: no-store. Error bodies are { "error": "English message" } (heartbeat errors: { "ok": false, "error": … }).
Public list (no authentication, CORS *)
GET /servers
Servers with a valid heartbeat in the last 90 s (plus demo servers when DEMO is on).
json
[{
"id": "192.168.2.3:30120", "host": "192.168.2.3", "port": 30120,
"name": "WildM LAN", "desc": "…", "tags": ["roleplay", "dev"], "locale": "FR",
"players": 0, "maxPlayers": 32, "peak": 0,
"serverId": "017992545b6d", "owner": "wildm-admin",
"iconUrl": "http://127.0.0.1:7777/media/servers/017992545b6d/icon.png?v=1794651208650",
"bannerUrl": null,
"verified": true
}]| Field | Source | Notes |
|---|---|---|
id | heartbeat | Always host:port; stable key for launcher favourites and history |
host, port | heartbeat | port is the game server's HTTP port |
name, desc, tags, locale | Portal | |
maxPlayers | Portal (else heartbeat) | |
players, peak | heartbeat | peak = 24-hour rolling peak for this server |
serverId, owner | Portal | null for demo servers |
iconUrl, bannerUrl | Portal | Absolute URLs or null; ?v= changes on each upload |
verified | — | true = registered on the Portal |
GET /stats
json
{ "players": 67, "peak24": 117, "servers": 4 }Total players now, network-wide 24-hour peak (sampled every 30 s), number of listed servers.
GET /version
json
{ "client": "0.1.0" }The launcher compares this with its own version at boot and warns if a newer one is published.
GET /media/servers/<serverId>/icon.png · …/banner.png · …/banner.jpg
Public images. Access-Control-Allow-Origin: *, Content-Security-Policy: default-src 'none', cached 5 minutes. 404 if absent.
Heartbeat (game server → master)
POST /heartbeat
JSON body, max 4 KB, sent every 30 s by the game server.
json
{ "key": "wfx_<40 hex>", "host": "192.168.2.3", "port": 30120, "players": 0, "maxPlayers": 32 }| Answer | When |
|---|---|
200 {"ok":true,"id":"host:port","serverId":"…"} | Valid key — the server is listed |
403 {"ok":false,"error":"Missing sv_key — create one on the WFX.re Portal"} | No key |
403 {"ok":false,"error":"Invalid sv_key — create one on the WFX.re Portal"} | Unknown key (format ^wfx_[0-9a-f]{40}$) |
429 | More than 30 invalid keys in 5 minutes from the same IP |
400 | Invalid JSON, or port not an integer 1–65535 |
host missing → the source IP is used. players is clamped to 0–4096. Other fields (name, desc…) are ignored: the Portal is the source of truth. One key = one list entry (the latest heartbeat wins).
Accounts (launcher, Bearer token)
| Route | Body / header | Response |
|---|---|---|
POST /auth/register | {username, password} | {token, username} |
POST /auth/login | {username, password} | {token, username} |
GET /auth/me | Authorization: Bearer <token> | {username} or 401 {"error":"Invalid session"} |
POST /auth/logout | Authorization: Bearer <token> | {ok:true} |
POST /auth/ticket | Authorization: Bearer <token>, {serverId} (12 hex, from GET /servers) | {ticket: "wft_…", expires} · 401 · 404 unknown server · 429 (60 / 5 min / IP) |
POST /auth/ticket/verify | {key: <sv_key>, ticket} (game server) | {ok:true, username, license} · 403 bad key · 404 unknown, expired or other server's ticket |
A ticket lets a game server know who a player is without ever seeing the account token. It is valid 2 hours, reusable for reconnects, and only on the server it was issued for (checked through that server's sv_key). license is a 40-hex HMAC of the account, stable across tickets and servers, that does not reveal the username. The launcher puts the ticket in wildm_connect.json → token; the client sends it in hello.
Username: 3–20 characters [A-Za-z0-9_.-], unique case-insensitively. Password: 8–200 characters. Rate limit: 10 attempts per 5 minutes per IP (429 beyond).
Launcher linking through the browser
text
launcher master browser
| POST /auth/link/start -> | |
| <- {code, poll, url, expiresIn} |
| opens url ------------------------------------------------>| GET /link?code=XXXX-XXXX
| | <- POST /auth/link/approve ----| {code, mode, username, password}
| GET /auth/link/poll?code&poll (every 2 s) |
| <- {status:"approved", token, username} || Route | Details |
|---|---|
POST /auth/link/start | {code, poll, url, expiresIn}. code is 8 characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789 shown as XXXX-XXXX; poll is a 48-hex secret only the launcher knows; expiresIn = 600 s. |
GET /link?code=… | The HTML sign-in page (CSP, X-Frame-Options: DENY). Without ?code= it points to the Portal. |
POST /auth/link/approve | {code, mode: "login" or "register", username, password} → {username}. Errors: Code expired…, This code has already been used, validation errors. Rate-limited like login. |
GET /auth/link/poll?code=…&poll=… | {status:"pending"}, {status:"approved", token, username} (delivered once, then the code is deleted), {status:"expired"} or {status:"invalid"} (wrong poll). |
Portal API (/portal/api/*, cookie session)
The Portal web app (GET /portal) uses a cookie session: wfx_session, HttpOnly; SameSite=Lax; Path=/; Max-Age 30 days (plus Secure when PUBLIC_URL is https). Sessions expire after 30 days.
CSRF protection: every non-GET request must carry an Origin equal to the host (http(s)://<Host> or the PUBLIC_URL origin) and, if present, Sec-Fetch-Site: same-origin — otherwise 403. JSON bodies require Content-Type: application/json (415), image uploads image/png or image/jpeg.
| Route | Body | Response |
|---|---|---|
GET /portal/api/me | — | {username} or 401 |
POST /portal/api/register · /login | {username, password} | {username} + Set-Cookie (the token is never in the body) |
POST /portal/api/logout | — | {ok:true}, cookie cleared |
GET /portal/api/servers | — | The account's servers (owner view), oldest first |
POST /portal/api/servers | {name, desc?, tags?, locale?, maxPlayers?} | 201 {server, key} — the only time the key is returned |
GET /portal/api/servers/<id> | — | Owner view |
PATCH /portal/api/servers/<id> | any of {name, desc, tags, locale, maxPlayers} (≤ 4 KB) | Owner view |
DELETE /portal/api/servers/<id> | — | {ok:true}; registration, key and images removed, delisted |
POST /portal/api/servers/<id>/key | — | {server, key}; the old key stops working at once and the server leaves the list until a valid heartbeat |
PUT /portal/api/servers/<id>/icon | raw PNG, ≤ 256 KB | {server, warning?} (warning if not 96×96) |
PUT /portal/api/servers/<id>/banner | raw PNG or JPEG, ≤ 1 MB | {server} |
DELETE /portal/api/servers/<id>/icon · /banner | — | {server} |
Owner view: {id, name, desc, tags, locale, maxPlayers, keyHint, iconUrl, bannerUrl, created, updated, online, live}, where live is {host, port, players, maxPlayers, seen} or null. The key hash is never returned.
Validation: name 3–64 characters, description ≤ 140, up to 5 tags of 16 characters [a-z0-9_-], locale XX or XX-YY, maxPlayers 1–1024, 10 servers per account. A server owned by another account answers 404. Image types are checked against magic bytes (PNG signature + IHDR, JPEG FF D8 FF) and must match the Content-Type; dimensions up to 4096×4096; size is checked on Content-Length and while reading (413).
Error codes: 400 validation, 401 not signed in, 403 origin refused, 404 unknown or not yours, 413 too large, 415 wrong type, 429 too many attempts.
Website routes
| Route | Content |
|---|---|
GET / | wfx.re landing page (live stats and server list) |
GET /docs/… | This documentation: the static VitePress build from docs-site/docs/.vitepress/dist (override with DOCS_DIST), with its own CSP; /docs redirects to /docs/, extension-less links resolve to .html, unknown pages get the site's 404.html |
GET /portal | Portal web app |
GET /link | Launcher linking page |
GET /assets/<file> | Site assets |
GET /download/<file> | Downloads from public/downloads/ (the landing page links WildM-Setup.exe; not published yet) |
GET /wfx-logo.svg, /wfx-mark.svg | Logos |
Unknown routes answer 404 {"error":"not found"}.