Files
m2dev-client/docs/update-manifest.md
Jan Nedbal 6f70ef201a docs: add update manifest schema
Formal JSON schema for the release manifest, with canonical ordering
rules so signatures stay stable. Includes a small synthetic example
under docs/examples/.
2026-04-14 10:36:24 +02:00

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 files array.

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 files array, entries are sorted by path lexicographically.
  • 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 \u00XX for 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

  • version strings 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 version is 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 version is older than the locally-recorded current-manifest.json version. 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.