# MiraDock Game SDK Specification Version: 1.0-draft Status: contract for Phase 1 and Phase 2 of the game platform build Audience: AI coding agents and humans building games for MiraDock, and the MiraDock engineers implementing the host side This document is the product contract. Every game published to MiraDock is built to it, and every host feature (play page, bridge, shop, saves, leaderboards) is implemented to it. Changes are additive; nothing here is removed or changed in meaning without a major version bump. --- ## 1. What a game is A MiraDock game is **one self-contained HTML file**. - Maximum 2 MiB after sanitization. - Inline ` ``` --- ## 13. Host implementation notes (not part of the creator contract) - The bridge is a single `postMessage` channel with a per-load nonce, following the existing height-reporter pattern in `components/wikis/HtmlPageRenderer.tsx`. Every message carries `{ nonce, id, method, args }` and gets exactly one `{ nonce, id, result | error }` reply. - The injected SDK is a small script prepended to the sanitized `srcdoc`, versioned with the platform, never fetched by the game. - Entitlement checks, save writes, and leaderboard writes are server actions authenticated by the host page's session, never by anything the game supplies. - `player.id` is `hmac(server_secret, user_id + game_id)`, truncated and base32 encoded. - Rate limits are enforced host-side per method; the game gets `rate_limited`, never a silent drop. - The play host creates one `play_sessions` row per page load, independently of the SDK. Successful `ready()` marks it ready and links the existing deduplicated `game_play_loads` row. The sandbox cannot invoke session or Like actions, read the session token, or choose timestamps. - Guest session identity is a random UUID stored by the top-level play page at localStorage key `miradock.play.guest-key.v1`. Signed-in sessions store only the account ID; neither IPs nor fingerprints are used. If storage is unavailable, the host uses a random identity for that load. Deleting an account replaces its session identities with unrelated random values so the exactly-one-identity constraint remains valid. - The host sends heartbeats every 30 seconds while visible, pauses them while hidden, and resumes on visibility. `pagehide` sends `/api/play/end` only `{ sessionId, token }`; a domain-separated HMAC using the server-only `GAME_PLAYER_ID_SECRET` authenticates that session. End calls are idempotent, and stale end calls use the last heartbeat. Back/forward-cache restoration reloads the host for a fresh session. Creation is limited to 20 sessions per minute per account or guest key across Games using the durable bucket RPC; render retries reuse the same session. - The Like control lives outside the iframe. Its visible-time timer enables after a minute; the database requires a session of at least 60 seconds before toggling the existing artifact Like. The gate also applies to legacy artifact Like inserts. A reload retains the Like state and count, with the current session's timer starting again. - `game_metrics_refresh()` runs hourly at `/api/cron/metrics` with the same `CRON_SECRET` protection as payouts and writes `game_metrics_refresh` audit events. `game_metrics_refresh_one` updates a Game after a Like. These service-only aggregates also appear in `get_game_stats.metrics` and the Creator's Game detail page; this does not affect ranking. - The trailing seven-day window includes successful sessions **started** in the window, including guests. Unique Players use distinct, separately namespaced account IDs and guest keys. Return rate is the fraction of those Players with a successful session **before** the window. Median duration includes ended sessions and unended sessions whose last heartbeat is over ten minutes old, treating the latter as ended at that heartbeat without writing or sweeping them. Ongoing sessions are excluded from the median. Likes are lifetime totals; purchases and donations count distinct Player ledger transaction IDs within the window, excluding fee/earning legs. Empty groups are zero.