330 lines
12 KiB
Markdown
330 lines
12 KiB
Markdown
# Moonwell Launcher Web API Spec
|
||
|
||
## Scope
|
||
|
||
This document specifies the current launcher behavior for:
|
||
|
||
- authentication against the Moonwell Web API
|
||
- installation directory selection
|
||
- local file scan and hash calculation
|
||
- manifest-based synchronization
|
||
- launch and game authorization for the Rust MoonWell 1.12.1 client
|
||
|
||
The launcher no longer uses S3/MinIO directly. All server interaction goes
|
||
through the Web API.
|
||
|
||
## Configuration
|
||
|
||
The launcher API base URL is provided via:
|
||
|
||
- `MOONWELL_API_BASE_URL`
|
||
- optional `MOONWELL_AUTH_ADDRESS` for the game auth endpoint (`host[:port]`)
|
||
|
||
Current integration mode is compile-time:
|
||
|
||
- `flutter run -d windows --dart-define=MOONWELL_API_BASE_URL=https://host`
|
||
- `flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`
|
||
|
||
## API Flow
|
||
|
||
The launcher uses these endpoints from `openapi.json`:
|
||
|
||
1. `POST /api/launcher/login`
|
||
2. `POST /api/launcher/register`
|
||
3. `GET /api/launcher/manifest`
|
||
4. `GET /api/launcher/download/{path}`
|
||
5. `GET /api/launcher/news`
|
||
6. `GET /api/launcher/realms`
|
||
7. `GET /api/launcher/account`
|
||
8. `POST /api/launcher/game-ticket`
|
||
|
||
Registration sequence:
|
||
|
||
1. User opens the registration tab and provides `username`, `email`,
|
||
`password`, `password_confirmation`, optional `invite_code`, and accepts
|
||
`terms`.
|
||
2. Launcher sends the payload to `POST /api/launcher/register`.
|
||
3. After a successful `201` response, the form returns to the login tab and
|
||
displays the API success message.
|
||
4. Validation and invite errors returned by the API are shown in the form.
|
||
|
||
Authentication sequence:
|
||
|
||
1. User enters launcher credentials.
|
||
2. Launcher requests a bearer token from `POST /api/launcher/login`.
|
||
3. Launcher requests the manifest from `GET /api/launcher/manifest`.
|
||
4. Successful login persists `LauncherSession` locally.
|
||
5. On next launcher start, saved session is reused to fetch manifest again.
|
||
6. `LauncherSession` and `ClientManifest` are passed into the home screen.
|
||
7. If an installation directory is saved, synchronization starts
|
||
automatically with the fetched manifest.
|
||
8. While the launcher is open, it refreshes the manifest every five minutes.
|
||
9. A server build hash that differs from the verified local build hash starts
|
||
synchronization automatically.
|
||
|
||
Logout sequence:
|
||
|
||
1. User presses `Logout`.
|
||
2. Launcher cancels any active sync.
|
||
3. Launcher clears the locally persisted `LauncherSession`.
|
||
4. Launcher returns to the login screen.
|
||
|
||
File download sequence:
|
||
|
||
1. Launcher resolves files that are missing, stale, or changed.
|
||
2. For each required file, launcher calls `GET /api/launcher/download/{path}` with bearer auth.
|
||
3. API returns JSON with a temporary presigned `url` and `expires_in`.
|
||
4. Launcher downloads the file directly from storage using the presigned URL.
|
||
5. Download is written into `*.moonwell.part`.
|
||
6. Downloaded file is verified against manifest `sha256`.
|
||
7. Temporary file replaces the destination file only after successful verify.
|
||
|
||
News sequence:
|
||
|
||
1. After the launcher reaches the authenticated home screen, it requests `GET /api/launcher/news`.
|
||
2. The response payload is read from the top-level `data` array.
|
||
3. Each news item provides `id`, `title`, `body`, optional `image_url`, and `created_at`.
|
||
4. News failures do not block patching or play flow; the launcher shows a local error state only inside the news panel.
|
||
5. Pressing a news item opens `<MOONWELL_API_BASE_URL>/news/{id}` in the
|
||
system browser.
|
||
|
||
Realm status sequence:
|
||
|
||
1. After the authenticated home screen opens, the launcher requests
|
||
`GET /api/launcher/realms`.
|
||
2. The first item in the top-level `data` array supplies the realm name,
|
||
online state, and game build shown in the sync panel.
|
||
3. Realm information is refreshed every minute while the launcher remains
|
||
open.
|
||
4. A failed refresh keeps the last known realm state and is retried on the next
|
||
interval.
|
||
|
||
Account sequence:
|
||
|
||
1. After the authenticated home screen opens, the launcher requests
|
||
`GET /api/launcher/account` with bearer authentication.
|
||
2. Only `username` is stored in launcher UI state and displayed in the account
|
||
menu.
|
||
3. The response `balance` field is intentionally ignored and never displayed.
|
||
4. Account metadata failures do not block patching or launching the game.
|
||
|
||
Game authorization sequence:
|
||
|
||
1. Immediately before starting the game, the launcher calls
|
||
`POST /api/launcher/game-ticket` with bearer authentication and
|
||
`{ "client_build": 5875 }` in the JSON body.
|
||
2. The API returns `account`, a 16-character `A-Z0-9` single-use `ticket`, and
|
||
`expires_at`. The account is 3–16 uppercase `A-Z0-9` characters. The ticket
|
||
must remain valid for at least five more seconds, including at process start.
|
||
3. Issuing a ticket atomically revokes any previous unused game ticket for the
|
||
same account. Production lifetime must not exceed 60 seconds.
|
||
4. The launcher never persists or logs the ticket.
|
||
5. The auth server uses the ticket as the temporary SRP password and consumes
|
||
it atomically after a successful logon proof.
|
||
|
||
## Installation Directory Rules
|
||
|
||
The user selects a single installation root directory.
|
||
|
||
All paths in the manifest are treated as relative to that root.
|
||
|
||
Path safety rules:
|
||
|
||
- backslashes are normalized to `/`
|
||
- leading `./` is removed
|
||
- absolute paths are rejected
|
||
- path traversal via `..` is rejected
|
||
|
||
## Ignored Directories
|
||
|
||
Unmanaged files in these directories outside `Data/` and `WoW/Data/` are
|
||
preserved and excluded from verification:
|
||
|
||
- `Cache`
|
||
- `Errors`
|
||
- `Logs`
|
||
- `Screenshots`
|
||
- `WTF`
|
||
- `benilla-config`
|
||
- `.moonwell_launcher`
|
||
- `Interface/AddOns`
|
||
|
||
Files under these directories:
|
||
|
||
- are not included in local scan
|
||
- do not participate in local `buildHash`
|
||
- are not compared against server manifest
|
||
|
||
Inside `Data/` and `WoW/Data/`, the manifest is authoritative for every file,
|
||
including nested files whose names contain `wtf`, `cache`, or `addons`.
|
||
The exclusions above do not exempt files inside Data from cleanup.
|
||
|
||
## Local Scan
|
||
|
||
For every existing manifest file in the installation directory, the launcher computes:
|
||
|
||
- normalized relative path
|
||
- file size in bytes
|
||
- file `sha256`
|
||
|
||
The result is stored as a `ClientInstallationSnapshot`. Paths are always relative
|
||
to the selected installation root, including files in nested directories.
|
||
|
||
The scan separately collects stale paths without hashing their contents:
|
||
|
||
- every file absent from the manifest in `Data/` and `WoW/Data/`
|
||
- `WoW.exe` and `*.dll` absent from the manifest directly in the installation
|
||
root or `WoW/` (binary names are matched without case sensitivity)
|
||
- unused Data subdirectories, ordered after their children; the Data roots and
|
||
directories needed by manifest files are retained
|
||
|
||
These paths do not contribute to `buildHash`, but prevent sync from completing
|
||
until cleanup and final verification succeed. Other unmanaged files are preserved.
|
||
On Windows, manifest membership checks for cleanup ignore path letter case.
|
||
Directory links are not traversed during cleanup. Links absent from the manifest
|
||
inside Data are removed as links, and deletion through a parent pointing outside
|
||
the installation is rejected. Directory deletion is never recursive.
|
||
|
||
## Hash Cache
|
||
|
||
To accelerate repeated scans, the launcher stores a local hash cache in:
|
||
|
||
- `<install-root>/.moonwell_launcher/hash_cache.json`
|
||
|
||
This metadata directory is excluded from verification.
|
||
|
||
Each cache entry is keyed by normalized relative path and stores:
|
||
|
||
- `size`
|
||
- `modifiedMs`
|
||
- `sha256`
|
||
|
||
Cache reuse rule:
|
||
|
||
- if `path`, `size`, and `modifiedMs` still match, the cached `sha256` is reused
|
||
- otherwise `sha256` is recomputed and the cache entry is replaced
|
||
|
||
The cache is an optimization only. The server remains the source of truth via
|
||
the manifest comparison.
|
||
|
||
## Diagnostics
|
||
|
||
During sync, the launcher writes a log file to:
|
||
|
||
- `<install-root>/.moonwell_launcher/launcher.log`
|
||
|
||
If an installation directory is not available yet, the launcher falls back to a
|
||
temporary system log directory.
|
||
|
||
For failed downloads:
|
||
|
||
- HTTP and storage download failures are logged with file path and status code
|
||
- checksum mismatches log expected and actual `size` and `sha256`
|
||
- invalid payloads are preserved as `*.moonwell.part.failed` for inspection
|
||
|
||
## buildHash Algorithm
|
||
|
||
`buildHash` is deterministic and calculated identically for local snapshot and
|
||
remote manifest.
|
||
|
||
For each file entry, the launcher builds a canonical string:
|
||
|
||
`<normalized-path>:<size>:<sha256-lowercase>`
|
||
|
||
Then:
|
||
|
||
1. sort all canonical strings lexicographically
|
||
2. join them with `\n`
|
||
3. compute `sha256` of the resulting UTF-8 payload
|
||
|
||
This means file ordering in the manifest does not affect `buildHash`.
|
||
|
||
## Sync Algorithm
|
||
|
||
The current sync flow is:
|
||
|
||
1. Load manifest.
|
||
2. Scan local installation.
|
||
3. Compute local `buildHash`.
|
||
4. Compare local files to manifest files by `path`, `size`, and `sha256`.
|
||
5. Identify stale paths in the cleanup areas described above.
|
||
6. If local `buildHash` matches server `buildHash` and there are no stale files
|
||
and no mismatches, finish successfully.
|
||
7. Delete stale paths, including unused Data subdirectories after their children.
|
||
8. Download changed and missing files.
|
||
9. Verify every downloaded file by `sha256`.
|
||
10. Re-scan the installation.
|
||
11. Recompute local `buildHash`.
|
||
12. Fail if final snapshot still differs from manifest or stale paths remain.
|
||
|
||
## Pause and Resume Semantics
|
||
|
||
Pause is implemented as cooperative cancellation:
|
||
|
||
- the current sync stream is cancelled
|
||
- the next sync starts a fresh comparison from current disk state
|
||
- already replaced files remain valid
|
||
- partial `*.moonwell.part` files are removed before the next download attempt
|
||
|
||
## Launch Behavior
|
||
|
||
When the user presses `Play`:
|
||
|
||
1. launcher requests a single-use game ticket for build 5875
|
||
2. launcher resolves `<install-root>/MoonWell.exe` on Windows,
|
||
`<install-root>/MoonWell.app/Contents/MacOS/launch` on macOS, or
|
||
`<install-root>/MoonWell` on Linux
|
||
3. launcher reads the auth address from `MOONWELL_AUTH_ADDRESS` when configured,
|
||
otherwise from `set realmlist <host[:port]>` in `realmlist.wtf`; this file is
|
||
never modified and the default auth port is 3724
|
||
4. launcher resolves the player's data in `Data/`, falling back to `WoW/Data/`,
|
||
and verifies that the ticket still has at least five seconds of validity
|
||
5. launcher starts MoonWell with working directory set to the installation root,
|
||
no authorization command-line arguments, and these child environment values:
|
||
- `WOW_USER=<account>`
|
||
- `WOW_PASS=<ticket>`
|
||
- `WOW_HOST=<auth-address>`
|
||
- `WOW_DATA=<absolute-data-directory>`
|
||
- `BENILLA_HOME=<install-root>/benilla-config`
|
||
6. launcher retains a process handle; while the process is alive, it disables repeated game launches
|
||
and client synchronization
|
||
7. when the process exits, the launcher returns to the ready-to-play state
|
||
|
||
Launching never edits `WTF/Config.wtf` or clears `Cache`. macOS executes the
|
||
bundle launcher directly; that script `exec`s MoonWell so the handle follows
|
||
the game rather than an `open` helper. Settings remain outside the `.app`.
|
||
If the executable, data directory, or auth endpoint is missing or invalid,
|
||
launch fails with an error. A consumed ticket cannot authenticate again; close
|
||
the client and press Play for a fresh ticket after a session ends.
|
||
|
||
## Error Behavior
|
||
|
||
The launcher surfaces errors for:
|
||
|
||
- invalid API configuration
|
||
- authentication failure
|
||
- registration validation or server failure
|
||
- manifest load failure
|
||
- path traversal attempts
|
||
- checksum mismatch after download
|
||
- missing downloaded temp files before verification
|
||
- final verification mismatch after update
|
||
- missing MoonWell executable, WoW data, or game auth endpoint
|
||
- invalid, expired, or unavailable game ticket
|
||
|
||
## Tested Invariants
|
||
|
||
The automated tests cover:
|
||
|
||
- deterministic `buildHash`
|
||
- path normalization used by `buildHash`
|
||
- preservation of user state and unrelated unmanaged files during cleanup
|
||
- removal of unmanaged Data files, unused subdirectories, WoW.exe, and DLLs
|
||
- preservation of manifest-listed DLLs and WoW.exe
|
||
- cache cleanup behavior
|
||
- safe path resolution
|
||
- sync flow with fully up-to-date client
|
||
- sync flow with stale files and changed files
|
||
- cleanup when all manifest files are already up to date
|
||
- rejection of stale paths remaining after final verification
|