- Migrate patch downloads to auth server redirect gateway (302 -> CDN) Allows instant CDN switching via admin panel without launcher update - Fix identity token "Player" username mismatch on fresh install Add token username verification with retry in fetchAuthTokens - Refactor versionManager to use mirror manifest via auth.sanasol.ws/patches - Add optimal patch routing (BFS) for differential updates - Add PATCH_CDN_INFRASTRUCTURE.md documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.1 KiB
Patch CDN Infrastructure & Game Update System
Overview
The F2P launcher downloads game patches through a CDN redirect gateway hosted on the auth server. This allows instant CDN switching (e.g., for DMCA takedowns) without releasing a new launcher version.
Architecture
Launcher --> GET auth.sanasol.ws/patches/manifest.json
--> 302 --> mega.io/.../manifest.json
Launcher --> GET auth.sanasol.ws/patches/windows/amd64/release/0_to_11.pwr
--> 302 --> mega.io/.../windows/amd64/release/0_to_11.pwr
The auth server acts as a pure redirect gateway (302). No bandwidth is consumed on the auth server - all actual file transfers happen directly between the launcher and the CDN.
URLs
| URL | Purpose |
|---|---|
https://auth.sanasol.ws/patches/* |
Redirect gateway (302 -> CDN) |
https://auth.sanasol.ws/patches/manifest.json |
Patch manifest (redirects to CDN) |
https://auth.sanasol.ws/admin/page/settings |
Admin panel to change CDN URL |
https://auth.sanasol.ws/admin/api/settings/patches-cdn |
API to GET/POST CDN base URL |
Default CDN (MEGA S4)
Base URL: https://s3.g.s4.mega.io/kcvismkrtfcalgwxzsazbq46l72dwsypqaham/hytale/patches
Changing CDN (DMCA response)
- Go to
https://auth.sanasol.ws/admin/page/settings - Find "Patches CDN Base URL" section
- Change URL to new CDN (e.g.,
https://new-cdn.example.com/patches) - Click "Save" - all launcher requests instantly redirect to new CDN
- No launcher update needed
Manifest Format
The manifest is a JSON file listing all available patch files:
{
"updated": "2026-02-20T13:20:09.776Z",
"files": {
"windows/amd64/release/0_to_11.pwr": { "size": 1618804736 },
"windows/amd64/release/10_to_11.pwr": { "size": 62914560 },
"darwin/arm64/release/0_to_11.pwr": { "size": 1617100800 },
"server/release": { "version": "2026.02.19-1a311a592", "size": 1509949440, "sha256": "..." },
...
}
}
Key Structure
File keys follow the pattern: {os}/{arch}/{branch}/{from}_to_{to}.pwr
- OS:
windows,linux,darwin - Arch:
amd64,arm64 - Branch:
release,pre-release - Patch:
{from}_to_{to}.pwr(e.g.,0_to_11.pwrfor full install,10_to_11.pwrfor differential)
Server builds use: server/{branch} with version, size, sha256 fields.
Game Update Process
1. Version Check
Launcher calls: getLatestClientVersion(branch)
-> Fetches manifest from auth.sanasol.ws/patches/manifest.json
-> Finds highest build number for current platform/branch
-> Returns "v{buildNumber}" (e.g., "v11")
2. Update Plan (Optimal Patch Routing)
Launcher calls: getUpdatePlan(currentBuild, targetBuild, branch)
-> Fetches manifest
-> Finds available patches for platform
-> Uses BFS to find optimal path (minimizes total download size)
-> Example: build 5 -> 11 might use: 5->10 (148MB) + 10->11 (60MB)
instead of: 0->11 (1.5GB)
3. Download & Apply
For each step in the update plan:
1. Download .pwr file from auth.sanasol.ws/patches/{key}
(redirects to CDN, supports resume via Range headers)
2. Apply patch using butler tool:
butler apply --staging-dir <staging> <pwr_file> <game_dir>
3. Save version after each step
4. Fresh Install
For first-time installs (currentBuild = 0):
- Downloads
0_to_{target}.pwr(full install, ~1.5GB) - Applies with butler to create the full game directory
5. Differential Update
For existing installations:
- Finds optimal patch chain (e.g.,
10_to_11.pwrat ~60MB) - Applies incrementally, saving progress after each step
- Falls back to full install if no patch path found
Mirror Sync Script
The mirror script (scripts/hytale-mirror.js) downloads patches from the official Hytale API and uploads to MEGA S4.
Usage
cd scripts
node hytale-mirror.js download # Download patches locally
node hytale-mirror.js upload # Upload to MEGA S4 via rclone
node hytale-mirror.js sync # Download + Upload in one step
What It Does
- Discovery: Calls Hytale API to find available patches for all platforms
- Download: Downloads .pwr files to
scripts/mirror/directory - Manifest Generation: Creates
manifest.jsonwith file sizes (no local paths) - Upload: Uses
rcloneto sync to MEGA S4
SOCKS5 Proxy
- API discovery calls use SOCKS5 proxy rotation (for rate limiting)
- File downloads do NOT use proxy (too slow for large files)
- Proxy list in
proxies.json(auto-refreshed from proxy service)
Prerequisites
rcloneconfigured withmegas4remote pointing to MEGA S4- Node.js 20+
- Network access to Hytale API endpoints
Launcher Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
HYTALE_AUTH_DOMAIN |
auth.sanasol.ws |
Auth domain (used for patch redirects) |
Key Files
| File | Description |
|---|---|
backend/services/versionManager.js |
Manifest fetching, version checking, update planning |
backend/managers/differentialUpdateManager.js |
Download orchestration, butler integration |
backend/utils/fileManager.js |
File download with retry, resume, stall detection |
backend/managers/gameLauncher.js |
Game launch with token fetch, patching, signing |
Constants (versionManager.js)
const AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
const MIRROR_BASE_URL = `https://${AUTH_DOMAIN}/patches`;
const MIRROR_MANIFEST_URL = `${MIRROR_BASE_URL}/manifest.json`;
const MANIFEST_CACHE_DURATION = 60000; // 1 minute cache
const FALLBACK_LATEST_BUILD = 11; // If manifest unreachable
Auth Server Implementation
Routes
GET /patches/* -> handlePatchRedirect()
- Extracts path after /patches/
- Reads CDN base URL from Redis settings
- Returns 302 redirect to {baseUrl}/{path}
- Tracks download metrics
GET /admin/api/settings/patches-cdn -> getPatchesCdnBaseUrl()
POST /admin/api/settings/patches-cdn -> setPatchesCdnBaseUrl()
Redis Storage
settings:global -> { patchesCdnBaseUrl: "https://s3.g.s4.mega.io/..." }
metrics:downloads -> { "patch:manifest.json": count, ... }
Troubleshooting
"Invalid manifest structure" error
- Check manifest.json is valid JSON with
filesobject - Verify CDN is accessible:
curl -sL https://auth.sanasol.ws/patches/manifest.json | python3 -m json.tool - Check admin settings for correct CDN URL
0-byte downloads
- Verify redirect works:
curl -sI https://auth.sanasol.ws/patches/darwin/arm64/release/0_to_11.pwr - Should show
302withLocationheader - Test actual download:
curl -sL -o /dev/null -w "%{size_download}" -r 0-1023 <url>
Manifest has local paths
- Regenerate manifest:
node scripts/hytale-mirror.js download(re-scans files) - Re-upload:
node scripts/hytale-mirror.js upload - Verify: entries should only have
{ size: <bytes> }, nopathfield
CDN switch not taking effect
- Check Redis: CDN URL stored in
settings:global - Verify via API:
curl https://auth.sanasol.ws/admin/api/settings/patches-cdn - Manifest is cached for 1 minute in launcher - wait or restart