forked from metin-server/m2dev-client
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/.
This commit is contained in:
52
docs/examples/manifest-example.json
Normal file
52
docs/examples/manifest-example.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"version": "2026.04.14-1",
|
||||
"created_at": "2026-04-14T14:00:00Z",
|
||||
"previous": "2026.04.13-3",
|
||||
"notes": "synthetic example showing the manifest structure. a real manifest covers tens of thousands of files.",
|
||||
"launcher": {
|
||||
"path": "Metin2Launcher.exe",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"size": 15728640,
|
||||
"platform": "windows"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"path": "Metin2.exe",
|
||||
"sha256": "2653e87ecd8ba305b70a96098788478e8b60c125ec83bcd1307486eafb5c5289",
|
||||
"size": 27982848,
|
||||
"platform": "windows"
|
||||
},
|
||||
{
|
||||
"path": "assets/root/serverinfo.py",
|
||||
"sha256": "b564fef7e45326ff8ad1b188c49cd61d46af1b07de657689814526725919d5cb",
|
||||
"size": 9231
|
||||
},
|
||||
{
|
||||
"path": "config.exe",
|
||||
"sha256": "a79c4e0daef43ce3a2666a92774fc6a369718177e456cb209fabe02ca640bcc2",
|
||||
"size": 258048,
|
||||
"platform": "windows"
|
||||
},
|
||||
{
|
||||
"path": "pack/item.pck",
|
||||
"sha256": "7aa9d46724a921fecf5af14c10372d0d03922e92a4cace4b5c15c451416f36b7",
|
||||
"size": 128547328
|
||||
},
|
||||
{
|
||||
"path": "pack/locale.pck",
|
||||
"sha256": "3b9dfe45317a14fcb70a138c1b9d57d984fe130e833c4341deaaff93d615ac67",
|
||||
"size": 4587520
|
||||
},
|
||||
{
|
||||
"path": "pack/metin2_patch_easter1.pck",
|
||||
"sha256": "2dab6a06d317014782cbe417d68dd3365d1d8c7cc35daa465611ce2108547706",
|
||||
"size": 12345600,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"path": "pack/uiscript.eix",
|
||||
"sha256": "79bd367b31882e52dfa902f62112f5d43831673ed548ebbd530e2f86dfa77d14",
|
||||
"size": 892416
|
||||
}
|
||||
]
|
||||
}
|
||||
124
docs/update-manifest.md
Normal file
124
docs/update-manifest.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 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](./update-manager.md) for the overall architecture this fits into.
|
||||
|
||||
## Top-level schema
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```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](./examples/manifest-example.json) for a real manifest produced by `scripts/make-manifest.py` over the current dev client.
|
||||
Reference in New Issue
Block a user