Files
EscapistsReintroduced/services/telemetry-worker/README.md
T
2026-10-30 17:13:11 +01:00

14 KiB
Raw Blame History

Co10 Escape telemetry receiver

Cloudflare Worker and D1 REST API for the existing mission telemetry protocol. The game sends one JSON event per POST to /api/mission-events. Any community server running the updated mission enrolls automatically on its first valid event. Operators do not need an account, a manually issued token, or a profile override. Mission → Statistics remains the administrator opt-out.

The Worker name is co10escape-telemetry; the bundled mod endpoint is https://co10escape-telemetry.zacekjakub.workers.dev/api/mission-events. Automatic enrollment requires a new Workshop release after 0.0.125 and the updated Worker below. These source files alone do not change either deployment.

Local verification

Requires Node.js 22 or newer and npm. From this directory:

npm ci
npm test
npm run check
cp .dev.vars.example .dev.vars
# Replace the example ADMIN_TOKEN with a random value (32–256 characters).
npm run migrate:local
npm run dev

The tests run the Worker in Miniflare/workerd with real local D1 SQL. They cover authentication, duplicate/concurrent retries, payload conflicts, out-of-order snapshots, final-result preservation, actual chunked HTTP requests, limits, legacy counters, storage rollback, server revocation, credential rotation, automatic enrollment races, ingestion rate limits, public-summary privacy and per-map win/failure totals across servers. They use disposable databases and synthetic identities. npm run check bundles the deployable Worker without publishing it. Miniflare is pinned to the version bundled with Wrangler so development and test runtimes match.

Deploy

The existing account already has D1 database erri-telemetry, bound as DB in wrangler.jsonc. The renamed Worker reuses this database and its history. From this directory, with Wrangler already logged in:

npm run migrate:remote
npm run deploy
npx wrangler secret put ADMIN_TOKEN

The migration adds the automatic/managed registration label. Deploying the new name creates co10escape-telemetry; secrets on erri-telemetry do not transfer. Enter the privately saved admin credential at the prompt for the new Worker. The old Worker is not deleted by these commands. After the new endpoint is verified, the old Worker can be retired separately.

For a fresh Cloudflare account, run npx wrangler login, then npx wrangler d1 create co10escape-telemetry. Update the database ID and name in wrangler.jsonc and run the migration/deploy commands above. Choose unused rate limiter namespace IDs if 1001–1003 already serve another application in the account. An independent read-only credential is optional:

npx wrangler secret put READ_TOKEN

Use independently generated random secrets of 32–256 characters. Enter them at Wrangler's prompt; do not put them in Git or in the addon. Before ADMIN_TOKEN is set, administration and detailed reads fail closed. Public aggregate summaries remain available. Public ingestion enrolls servers independently of this admin credential. Wrangler prints the Worker URL, for example https://co10escape-telemetry.YOUR-SUBDOMAIN.workers.dev. /health is a public liveness check, not a database readiness test. The service does not enable CORS; a future browser dashboard should use a backend to keep API credentials private.

Deployment uses Cloudflare's documented D1 migrations, Worker secrets, and transactional D1 batches.

Automatic community-server enrollment

The updated addon saves a random server ID in its telemetry state and generates an independent secret from two random UUIDs. It verifies the secret on disk in $profile:EscapistsTelemetryCredentials.json before sending any request. No receiver admin secret is included in the mod. The first valid event atomically creates the server record and stores the event. Only the credential hash is stored by D1. Following events and retries require that same installation credential.

Automatic records are labelled registration: "automatic"; operator-created records are labelled "managed". Neither label is an anti-cheat attestation. An automatic claim cannot replace an existing server's credential or reactivate a revoked server. Concurrent claims with different credentials yield one owner. Preserve the profile's state and credentials together when moving a server. Corrupted credential storage suspends delivery; it is never silently reset.

The Worker bounds request bodies and uses three Cloudflare rate limiters: 300 requests/minute per source, 10 new enrollment attempts/minute per source, and 30 valid requests/minute per server credential. Source keys are hashes of Cloudflare's connecting-IP header, used only in ephemeral limiter state. Limits are approximate and local to each Cloudflare location, not a global storage quota. Shared hosting addresses share source quotas; a 429 makes the game retry. See Cloudflare rate limiting.

Optional managed server credentials

Each game server has its own serverId in $profile:EscapistsTelemetryState.json, inside state.serverId. Register this existing ID; do not replace the state file, mission IDs or queued events.

The normal automatic path needs no operator action. The administrative API also supports pre-registering an existing profile ID with a manually issued token. For an already enrolled server, use token rotation instead of registering again.

Make an authenticated request (substitute the ID read from the profile):

POST /api/servers
Authorization: Bearer ADMIN_TOKEN
Content-Type: application/json

{"serverId":"11111111-1111-4111-8111-111111111111","name":"My Escape server"}

The 201 response contains serverId, endpoint, and a new token. The raw token is returned only on registration or rotation; D1 stores its SHA-256 hash. Save the token directly in the server's private profile configuration:

{
  "endpoint": "https://co10escape-telemetry.YOUR-SUBDOMAIN.workers.dev/api/mission-events",
  "serverName": "My Escape server",
  "token": "TOKEN_FROM_REGISTRATION"
}

Restrict access to that file. Restart the mission to load it. The in-game Statistics page should report delivered events. Confirm the mission with GET /api/missions?serverId=SERVER_ID. The game token is sent in the JSON envelope as required by the existing native HTTP sender; it cannot read data, register servers or submit events for another server ID.

POST /api/servers/SERVER_ID/token rotates a credential and reactivates a revoked server. Update the private profile with the returned token and restart the mission. DELETE /api/servers/SERVER_ID revokes ingestion without deleting history. Never distribute the administrator or read credential to game servers.

Map records and the results screen

Opening /, /api/summary, or /api/mission-events in a browser returns public JSON with collected win/failure totals. No token is required for these summaries. Use ?terrain=URL_ENCODED_ISLAND_NAME to restrict totals to one map, and add &serverId=SERVER_UUID to include that installation's record for the same map. Without a terrain filter, the response includes overall totals and up to 100 maps with separate win/failure counts. The response contains counts and reported island names, not player/server names, account IDs, raw events or credentials. Full records remain protected by admin/read credentials.

Each summary has schemaVersion: 1, terrain, allServers and optional server. Each record contains completed, wins, losses, partialMissions, and winRate (percentage; null when there are no completed rounds). The game server resolves the island name from its loaded root scene, independently of the scenario title. Names are compared case-insensitively. Older reports through 0.0.129 retain their world-resource identifiers; query those with the original resource path. The receiver does not infer or rewrite historical islands.

The acknowledgement for an accepted victory/defeat event also includes a community summary for that event's map, server, mission and event ID. It is computed after storage and includes the just-finished round exactly once. An equivalent retry returns updated totals without counting the round again. A conflicting second terminal snapshot cannot claim inclusion. A failure to read totals does not undo a successful event acknowledgement.

The updated mission prioritizes its final result, validates the response against the current map/server/mission/event/outcome, and replicates a short summary to the win/failure screen. The screen refreshes if totals arrive after it opens. It shows map-specific wins, failures and rounded win rates for this server and all reporting servers. Local round statistics remain available when reporting is disabled, storage/network fails, the receiver lacks summary support, or the mission ends before delivery completes. Community totals then show unavailable; they are not invented as zero. This feature requires the updated Workshop build.

API

All responses are JSON with Cache-Control: no-store. Administrative endpoints require Authorization: Bearer ADMIN_TOKEN. Read endpoints accept ADMIN_TOKEN or the optional READ_TOKEN. Public summaries and /health do not require a token; ingestion uses each installation's own credential.

Method and path Purpose
GET /health Public liveness check
GET /, GET /api/summary, GET /api/mission-events Public map win/failure summaries; optional terrain and serverId filters
POST /api/mission-events Ingest { "token": "...", "event": {...} }; first valid event enrolls an unknown server
POST /api/servers Register {serverId, name} and return a new token
GET /api/servers List server labels, IDs, registration origin, creation times and active state; never token hashes
POST /api/servers/:serverId/token Rotate token and reactivate server
DELETE /api/servers/:serverId Revoke ingestion; retain history
GET /api/missions Mission summaries, newest-created first
GET /api/missions/:serverId/:missionId Authoritative selected snapshot with players, mods, settings and result
GET /api/missions/:serverId/:missionId/events Raw event history, newest-received first
GET /api/stats Distinct mission counts grouped by outcome, with partial-mission counts
GET /api/players Authenticated participants' attempts, personal escapes and combat totals

/api/missions, /api/stats and /api/players accept an optional serverId filter. /api/missions also accepts outcome (victory, defeat, preparation_failed, restarted_empty, interrupted, or unfinished). Mission, server and event lists accept limit (1–100, default 50) and before from the preceding nextBefore. Player lists sort by stable account ID and use after from nextAfter, plus limit. A non-null cursor means another page may exist; the last page can be empty. Read results use integer 0/1 for SQLite-derived flags; raw events retain booleans.

Ingestion returns 201 after a new durable event, or 200 with duplicate: true after an equivalent retry. Victory/defeat acknowledgements include current map records when available. An already used server/event key with different content receives 409. Invalid payloads receive 400, unauthorized requests 401, oversized bodies 413, unsupported media types 415, rate limits 429, and storage failures 503. The current game client retries every non-2xx, including permanent errors; fix invalid credentials/schema or inspect the queue rather than treating 4xx as successful delivery. Request bodies are bounded while streaming, including when there is no Content-Length. Event JSON is limited to 65,536 UTF-16 code units; the HTTP envelope permits 264 KiB for UTF-8 expansion and the credential.

Storage and counting rules

events retains validated event objects, keyed uniquely by (serverId, eventId). Envelope tokens, request headers and IP addresses are not stored by this code. An SQL trigger updates missions in the same transaction. D1 must commit both before the Worker returns 2xx. A failed projection rolls back the event too.

For each (serverId, missionId), the first accepted terminal snapshot wins. Subsequent terminal events remain in the history but cannot change its result or count again. Before completion, snapshots advance by elapsed mission time, then event wall-clock time; late starts/restarts cannot clear a terminal result. Reported previousOutcome is retained as evidence in events, never converted into an invented result for a missing mission. Unknown endings stay unfinished.

Player totals include authenticated, participating players from victory/defeat snapshots only. Spectators and temporary connection IDs are excluded. A mission victory does not imply a personal escape. Startup failures, empty restarts and interrupted sessions do not count as gameplay wins/losses or player attempts. An interrupted mission can still have raw combat data in its detail snapshot.

Legacy missing counters remain null, with missionsWithKills, missionsWithFriendlyKills, missionsWithDeaths, missionsWithDowned and missionsWithRecoveries showing each total's coverage. Partial and truncated mission counts remain visible; missing players cannot be reconstructed. Results are community reports bound to installation credentials, not independent anti-cheat attestations.

Raw events and final snapshots are retained indefinitely by default. Monitor D1 storage as usage grows. Deleting raw events also deletes their deduplication keys; do not prune them while a server may still retry old queued events. A retention policy should preserve deduplication tombstones if raw payloads are removed. Keep read credentials private: mission details contain player names and account IDs. Worker request logging is disabled in the supplied configuration. Cloudflare platform logging/settings are managed separately in the account.