# m2pack integration

Notes on how `metin-launcher` handles the m2pack release format. Written when the
launcher-side support landed on `claude/m2pack-launcher`; the client-side receiver
for the runtime key is tracked separately.

## Scope

The launcher's job in an m2pack release is narrowly defined:

1. Fetch the signed `manifest.json` and its `.sig` exactly as before.
2. Verify Ed25519 signature against the compiled-in public key, exactly as before.
3. Read the new top-level `format` field (falling back to `legacy-json-blob`).
4. Dispatch through `ReleaseFormatFactory.Resolve(...)` to an `IReleaseFormat`.
5. Diff, download and atomically apply the file entries listed in the manifest.
   For m2pack these are `.m2p` pack archives plus a `runtime-key.json` sidecar.
6. **Never** open, decrypt or decompress a `.m2p` archive. Integrity of the
   archive contents is the client's problem, gated on the runtime key.
7. Load `runtime-key.json` from the client root and forward it to `Metin2.exe`
   via environment variables (`M2PACK_MASTER_KEY_HEX`, `M2PACK_SIGN_PUBKEY_HEX`,
   `M2PACK_KEY_ID`), scoped to the child process only.

Anything beyond step 7 — parsing the archive, selecting pack files, decrypting
blocks, validating per-file signatures — lives in the client. The launcher is
deliberately dumb about `.m2p`.

## Signature boundary

The Ed25519 signature stays on the top-level `manifest.json`. `.m2p` files are
each a single `files[]` entry with a `sha256`, exactly like any other blob —
that's enough to detect tampering or incomplete downloads, and tamper-resistance
of the content-addressed URL is inherited from the signed manifest.

The `runtime-key.json` sidecar is listed in the manifest in the same way, so
its integrity is also covered by the manifest signature. We do **not** sign the
runtime key independently.

## Runtime key delivery

Two delivery strategies are defined behind `IRuntimeKeyDelivery`:

- `EnvVarKeyDelivery` — MVP, used on every platform. Sets
  `M2PACK_MASTER_KEY_HEX`, `M2PACK_SIGN_PUBKEY_HEX` and `M2PACK_KEY_ID` on the
  child `ProcessStartInfo.Environment`. The launcher never sets these on its
  own process environment, so unrelated processes can't snoop them and the
  values don't leak via `Environment.GetEnvironmentVariable` calls elsewhere
  in the launcher.
- `SharedMemoryKeyDelivery` — Windows stub. Not implemented. See the class
  docs for the planned approach (named `MemoryMappedFile` with a random
  suffix, write-then-zero lifecycle, child reads via mapping name passed in
  env). Left as a stub because the client-side receiver hasn't landed.

Threat model note: env vars are readable by any process that shares the
parent's user on Linux (`/proc/$pid/environ`, gated by `ptrace_scope` and by
file mode). That's identical to the current legacy flow in terms of secret
handling — the legacy release also trusts the local user — and the shared
memory strategy is planned for a future hardening pass, not as an MVP
requirement.

## Telemetry

`ClientAppliedReporter` is wired but disabled by default: `LauncherConfig.TelemetryUrlTemplate`
is empty. When set to a non-empty URL template (supporting `{format}` and
`{version}` placeholders) the reporter fires a single bounded POST after a
successful apply, with a hard 5-second timeout and warning-level logging on
failure. Failures never block the launch flow and are never retried.

The payload is intentionally small: `format`, `version`, `timestamp`. Nothing
personally identifying.

## Backward compatibility

Existing installs pointing at a manifest without a `format` field continue to
parse exactly as before: `Manifest.EffectiveFormat` defaults to
`legacy-json-blob` and `LegacyJsonBlobFormat` is a near-verbatim lift of the
old inline behaviour. The only visible difference is that the orchestrator's
log line now reads `format=legacy-json-blob`.

Signed manifests that ship an unknown `format` value are refused outright —
`ReleaseFormatFactory.Resolve` throws and the orchestrator reports
`OfflineFallback`. This prevents a future attacker who compromises the signing
key from downgrading clients via a fabricated `format: "no-verify"` field.

## File layout on disk

```
client/
├── Metin2.exe
├── runtime-key.json           ← written from the manifest, loaded by M2PackFormat
├── pack/
│   ├── item.m2p               ← staged/placed by the launcher, never opened
│   ├── mob.m2p
│   └── ...
└── .updates/
    ├── launcher.log
    ├── current-manifest.json
    └── staging/               ← per-file resume, same as legacy
```

## Testing

Run `dotnet test -c Release` — the relevant suites are:

- `RuntimeKeyTests` — happy path, validation, load-from-disk
- `EnvVarDeliveryTests` — scoped env mutation, no current-process leak
- `LegacyJsonBlobFormatTests` / `M2PackFormatTests` / `ReleaseFormatFactoryTests`
- `ClientAppliedReporterTests` — disabled-by-default, success, 500, timeout,
  connection refused
- `UpdateOrchestratorFormatDispatchTests` — signature failure path, offline
  fallback when manifest fetch fails, result carries `Format` + `RuntimeKey`
- Extended `GameProcessTests` asserting the runtime key gets forwarded through
  `BuildStartInfo` and does NOT leak into the launcher's own environment.

Total suite after this change is ~92 tests.
