Formal JSON schema for the release manifest, with canonical ordering rules so signatures stay stable. Includes a small synthetic example under docs/examples/.
5.4 KiB
Update manifest — format specification
The update manifest is a JSON document describing a single release of the Metin2 client. It lives at https://updates.jakubkadlec.dev/manifest.json alongside its Ed25519 signature at manifest.json.sig.
See update-manager.md for the overall architecture this fits into.
Top-level schema
{
"version": "2026.04.14-1",
"created_at": "2026-04-14T12:00:00Z",
"previous": "2026.04.13-3",
"launcher": {
"path": "Metin2Launcher.exe",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 15728640
},
"files": [
{
"path": "Metin2.exe",
"sha256": "a1b2c3...",
"size": 27982848,
"platform": "windows",
"required": true
}
]
}
Required top-level fields
| Field | Type | Description |
|---|---|---|
version |
string | Release version. Format: YYYY.MM.DD-N where N is the 1-indexed daily counter. Sortable, human-readable, allows multiple releases per day. |
created_at |
string | ISO 8601 timestamp in UTC with Z suffix. When the release was produced. |
launcher |
object | The launcher binary. See below. Special because of self-update handling. |
files |
array | The non-launcher files in the release. May be empty (launcher-only update). |
Optional top-level fields
| Field | Type | Description |
|---|---|---|
previous |
string | version of the manifest this release replaces. Omit for the first release ever. Used for changelog display and future delta-patch strategies. |
notes |
string | Free-form release notes (Markdown). Displayed by the launcher in the changelog panel. |
min_launcher_version |
string | Refuse to apply this manifest if launcher's own version is older than this. Used when a manifest change requires a newer launcher. |
File entry schema
{
"path": "pack/item.pck",
"sha256": "def456abc123...",
"size": 128000000,
"platform": "all",
"required": true
}
Required file fields
| Field | Type | Description |
|---|---|---|
path |
string | Path relative to the client root, using forward slashes. No .. segments. |
sha256 |
string | Lowercase hex sha256 of the file contents. |
size |
integer | File size in bytes. Used for the progress bar and to detect truncated downloads before hashing. |
Optional file fields
| Field | Type | Default | Description |
|---|---|---|---|
platform |
string | "all" |
One of "windows", "linux", "all". Launcher filters by its own platform. |
required |
boolean | true |
If false, a failed download for this file does not block the game launch. |
executable |
boolean | false |
On Unix-like systems, set the executable bit after applying. Ignored on Windows. |
Launcher entry
The launcher top-level object has the same fields as a file entry, but is called out separately because the launcher is a privileged file:
- The launcher replaces itself via rename-before-replace, not normal atomic move.
- The launcher is always required; if it fails to update, the launcher does not launch the game, to avoid a broken loop where the player is running a buggy launcher that can't fix itself.
- The launcher is never listed in the
filesarray.
Signing
manifest.json.sig is the raw Ed25519 signature over the literal bytes of manifest.json, in detached form. The public key is compiled into the launcher binary. Signing and verification use the standard Ed25519 algorithm (RFC 8032), no prehashing.
Example verification in Python:
import json
from nacl.signing import VerifyKey
with open("manifest.json", "rb") as f:
manifest_bytes = f.read()
with open("manifest.json.sig", "rb") as f:
sig = f.read()
VerifyKey(bytes.fromhex(PUBLIC_KEY_HEX)).verify(manifest_bytes, sig)
In C# with System.Security.Cryptography (.NET 8+) or BouncyCastle.
Canonical JSON
To keep signatures stable across trivial reformatting:
- Top-level keys appear in the order
version, created_at, previous, notes, min_launcher_version, launcher, files. - Within the
filesarray, entries are sorted bypathlexicographically. - Within a file object, keys appear in the order
path, sha256, size, platform, required, executable. - JSON is pretty-printed with 2-space indentation, LF line endings, final newline.
- Strings use the shortest valid JSON escapes (no
\u00XXfor printable ASCII).
scripts/make-manifest.py produces output in exactly this form. Do not re-serialize a manifest with a different JSON library before signing; the bytes must match.
Versioning rules
versionstrings are compared as date + counter (not as semver), via(date, counter)tuples.- A launcher always replaces its own installed version with the one from the latest manifest, regardless of whether the manifest's
versionis newer than the launcher's own version. There is no "downgrade protection" for the launcher itself because the server is the source of truth. - For the client files (not launcher), the launcher refuses to apply a manifest whose
versionis older than the locally-recordedcurrent-manifest.jsonversion. This prevents rollback attacks where a compromised CDN replays an old manifest to force players back onto an outdated client that had a known vulnerability.
Example
See examples/manifest-example.json for a real manifest produced by scripts/make-manifest.py over the current dev client.