{"openapi":"3.0.3","info":{"title":"Shark Tank Control API","version":"1.1.0"},"servers":[{"url":"/","description":"This server"}],"security":[],"tags":[{"name":"system","description":"Health and service metadata"},{"name":"tank","description":"Ocean tanks, presence, and the global leaderboard"},{"name":"profile","description":"Per-player cosmetics + settings"},{"name":"realtime","description":"WebSocket gameplay"},{"name":"ops","description":"Public status, incidents, billing inquiry, logs, the public ISO 27001 / ISO 42001 conformance register, and the authenticated Admin controls"}],"paths":{"/api/health":{"get":{"tags":["system"],"summary":"Liveness probe","description":"Returns module name and server time.","responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/tank":{"get":{"tags":["tank"],"summary":"List joinable tanks","description":"Stable catalog of ocean-named tanks with live player counts and top score. One earlier path name still reaches this same handler.","responses":{"200":{"description":"Tank list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TankResponse"}}}}}}},"/api/leaderboard":{"get":{"tags":["tank"],"summary":"Global leaderboard","description":"Top scores across all tanks, persisted by the control plane.","responses":{"200":{"description":"Top scores","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardResponse"}}}}}}},"/api/profile":{"get":{"tags":["profile"],"summary":"Read a player profile","parameters":[{"name":"id","in":"query","required":false,"description":"Player id (default: local)","schema":{"type":"string"}}],"responses":{"200":{"description":"Profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}}}},"post":{"tags":["profile"],"summary":"Upsert a player profile","description":"Unauthenticated: the identity is a `wg_player` cookie the first GET mints, so the cookie cannot be the throttle key. Writes are bucketed per edge connection and, separately, under a global ceiling across every public caller at once — an overwrite of an existing row costs the same Durable Object write as a new one, so both branches are metered. Bodies over 16 KiB are refused, and the ceiling is enforced on the bytes that actually arrive rather than on a declared `Content-Length`. When the spend gate is closed this route is refused with 503 along with the game, because it is one of the writes the spend limit exists to stop.","parameters":[{"name":"id","in":"query","required":false,"description":"Player id (default: local)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}},"responses":{"200":{"description":"Saved profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"}}}},"413":{"description":"Payload too large"},"429":{"description":"Rate limited, per connection or across all public callers"},"503":{"description":"Spend gate closed"}}}},"/api/audit":{"post":{"tags":["system"],"summary":"Record a public gameplay event","description":"The only unauthenticated write into the 90-day service action log, and the reason that log carries two limits the trusted server-side callers do not. Accepts exactly two event types: `play` (which requires a `room` from the fixed tank list) and `customize` (whose `detail` must match `skin <id>` or is replaced with a fixed phrase). Bodies over 16 KiB are refused, and the ceiling is enforced on the bytes that actually arrive rather than on a declared `Content-Length`, so a chunked body cannot slip past it. Writes are bucketed per edge connection and, separately, under a global ceiling across every public caller at once; publicly written rows are then trimmed to their own floor before the whole-log trim runs, so a flood can only evict other public rows and never server-recorded evidence. The display name attached to the row is resolved on the server behind that rate limit, never taken from the request.","responses":{"200":{"description":"Event recorded","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Unsupported public event type, or a play event without a valid tank"},"413":{"description":"Payload too large"},"429":{"description":"Rate limited, per connection or across all public callers"},"503":{"description":"Spend gate closed"}}}},"/api/security-report":{"post":{"tags":["system"],"summary":"Report a security issue","description":"Same-origin public white-hat intake. One request creates a linked report, a retained audit event, and an append-only SHA-256 control-history receipt, and raises the report to operations. It does not change service state: the game stays online, no incident is opened, and no tank is disconnected. Whether a report warrants downtime is a separate authenticated operator decision made at /admin/security-report. Accepted reports are throttled globally to one per minute. Returned metadata is limited to environment, deployment, colo, and country; it contains no secrets or IP data. A report is not confirmation of compromise.","parameters":[{"name":"X-WG-Security-Report","in":"header","required":true,"schema":{"type":"string","enum":["white-hat"]}}],"responses":{"200":{"description":"Security report receipt","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Same-origin report required"},"429":{"description":"A security report was accepted moments ago"},"502":{"description":"Report could not be persisted"}}}},"/room/{id}/ws":{"get":{"tags":["realtime"],"summary":"Realtime play (WebSocket upgrade)","description":"Upgrades the connection (HTTP 101) into the tank's realtime Room. **Client → server** messages: `{t:'hello',name,skin,debugLanguage}`, `{t:'debug',language}` (selected TypeScript/PHP capture tag), `{t:'input',action}` (action = setHeading/setBoost/rocket/respawn), `{t:'ping',ts}`. **Server → client**: `welcome`, `state` (per-tick snapshot), `leaderboard`, `died`, `pong`.","parameters":[{"name":"id","in":"path","required":true,"description":"Tank id (e.g. room-1)","schema":{"type":"string"}},{"name":"roomName","in":"query","required":false,"description":"Display name for the tank","schema":{"type":"string"}}],"responses":{"101":{"description":"Switching Protocols — WebSocket established"},"426":{"description":"Upgrade Required — request was not a WebSocket upgrade"}}}},"/docs/":{"get":{"tags":["ops"],"summary":"API documentation (this page)","description":"Renders this OpenAPI document as HTML. Raw document at /docs/openapi.json.","responses":{"200":{"description":"HTML documentation","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/docs/openapi.json":{"get":{"tags":["ops"],"summary":"OpenAPI document","description":"This document. Also served at `/openapi.json`, which is the path the conformance register's evidence index links to.","responses":{"200":{"description":"OpenAPI 3.0 document","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/trust/":{"get":{"tags":["ops"],"summary":"Trust and operations overview","description":"The entry point to the published evidence: availability, incidents, metered spend, conformance readiness, the last deployment and the receipt chain verdict. Every figure is computed from the same source the owning page uses and links to it; none is stored a second time.","responses":{"200":{"description":"Trust overview","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/roadmap/":{"get":{"tags":["ops"],"summary":"Change record (moved)","description":"Permanently redirects to `/status/#delivery`, where the change record now lives. `/roadmap.json` did not move and is unchanged.","responses":{"301":{"description":"Moved to /status/#delivery"}}}},"/roadmap.json":{"get":{"tags":["ops"],"summary":"Mission and feature-to-deployment map (JSON)","responses":{"200":{"description":"Availability, delivery velocity, elapsed time, deployment batches, and feature updates","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/status/":{"get":{"tags":["ops"],"summary":"Operations dashboard","description":"Operations. Server and tank availability, live tank occupancy with the computer-controlled agent count beside it, state copies and restore drills, the full incident record at `#incidents`, the append-only control receipt chain at `#control-history`, and the change record at `#delivery`. Spend is at `/spend/`; operator controls are behind authentication at `/admin/`.","responses":{"200":{"description":"HTML dashboard","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/status.json":{"get":{"tags":["ops"],"summary":"Status (JSON)","responses":{"200":{"description":"Independent server and tank availability, scheduled downtime, rooms, incidents, and control receipts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/incidents/":{"get":{"tags":["ops"],"summary":"Incident record (moved)","description":"Permanently redirects to `/status/#incidents`. `/incidents.json` did not move and is unchanged.","responses":{"301":{"description":"Moved to /status/#incidents"}}}},"/incidents.json":{"get":{"tags":["ops"],"summary":"Public incident and control history (JSON)","responses":{"200":{"description":"Availability summary, incident records, control entries, and SHA-256 chain head","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/spend/":{"get":{"tags":["ops"],"summary":"Cost and capacity meters","description":"Links normal game and operations actions to measured Workers, Durable Objects, D1 and R2 usage, against each free-tier allowance and against the hard spend limit that closes the game rather than billing. Previously served at `/inquiry/`, which still redirects here.","responses":{"200":{"description":"Cost and capacity meters","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/spend.json":{"get":{"tags":["ops"],"summary":"Cost and capacity meters (JSON)","description":"Also served at `/inquiry.json`, the pre-rename name, which is unchanged.","responses":{"200":{"description":"Proof-of-concept statement and reset-window billing summary","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/inquiry/":{"get":{"tags":["ops"],"summary":"Cost and capacity meters (moved)","description":"Permanently redirects to `/spend/`.","responses":{"301":{"description":"Moved to /spend/"}}}},"/logs/":{"get":{"tags":["ops"],"summary":"Public reason-coded logs","description":"Searchable, filterable service evidence with 90-day retention plus the 40 newest reason-coded captures per ocean tank and sanitized TXT downloads.","responses":{"200":{"description":"Public Shark Tank evidence","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/logs.json":{"get":{"tags":["ops"],"summary":"Public service and tank logs (JSON)","description":"Every row includes a letter-plus-three-digit reason code. Tank records use the same timestamp, reasonCode, tick, action, language, name, and details fields as the live inspector and TXT export.","responses":{"200":{"description":"Public service and tank event stream","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/logs/game/{id}.txt":{"get":{"tags":["ops"],"summary":"Download a sanitized Shark Tank log","description":"Plain UTF-8 text with timestamp,reason_code,tick,action,language,name,details comma-separated fields and newline-separated records. Internal player ids are omitted.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Downloadable TXT log","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Unknown tank"}}}},"/audit/":{"get":{"tags":["ops"],"summary":"ISO/IEC 27001 and ISO/IEC 42001 conformance register","description":"Public, unauthenticated readiness register: every clause of ISO/IEC 27001:2022 and ISO/IEC 42001:2023, all 93 Annex A controls, all 38 AI controls, the fourteen documented change-management processes, the Stage 1 documented-information list, and an index of every route that serves as evidence. Each row carries a status, the justification behind it, and links to the live routes that prove it. This is a readiness statement, not a certificate.","responses":{"200":{"description":"Conformance register","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/audit/manifest.json":{"get":{"tags":["ops"],"summary":"Conformance register (JSON)","description":"The same register as machine-readable data, for independent scoring or import into a compliance tool.","responses":{"200":{"description":"Standards, status meanings, readiness summary, change processes, mandatory documents, control registers, and the evidence index","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/policies/":{"get":{"tags":["ops"],"summary":"Governance policy index","description":"The index of the written record ISO/IEC 27001:2022 and ISO/IEC 42001:2023 ask for, published as pages rather than filed. Each document is its own route at `/policies/{document}/`, searchable from here, and every section within a document has its own anchor.","responses":{"200":{"description":"Policy index","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/policies/{document}/":{"get":{"tags":["ops"],"summary":"One governance document","description":"A single document — context and scope, the information security policy, roles and authorities, the risk assessment and treatment processes, the Statement of Applicability cover, the risk treatment plan, the security and AI objectives, the AI policy with its impact assessment, the AI system life cycle, and the rest. Each names the clauses it is the record for, and the conformance register links to it directly. The document identifier is the `id` field in `/policies.json`.","parameters":[{"name":"document","in":"path","required":true,"schema":{"type":"string"},"description":"Document identifier, e.g. `risk-assessment`"}],"responses":{"200":{"description":"Governance document","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"No such document","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/policies.json":{"get":{"tags":["ops"],"summary":"Governance policy set (JSON)","description":"The same documents as machine-readable data: reference, identifier, the route it is published at, title, purpose, the clauses each satisfies, its sections with their anchor ids, and what triggers its review.","responses":{"200":{"description":"Governance documents with their clause coverage and review triggers","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/admin/":{"get":{"tags":["ops"],"summary":"Operations control panel","description":"Protected HTML dashboard for action records, measured billing counters, billing reset, and maintenance mode. Formerly served at /audit/, which is now the public conformance register.","security":[{"opsBasic":[]}],"responses":{"200":{"description":"Control panel","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/log.json":{"get":{"tags":["ops"],"summary":"Action log (JSON array)","description":"90-day user and service action record. Also served at the pre-move path /audit.json.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500}}],"security":[{"opsBasic":[]}],"responses":{"200":{"description":"Audit events","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/log.jsonl":{"get":{"tags":["ops"],"summary":"Action log (newline-delimited JSON)","description":"Also served at the pre-move path /audit.jsonl.","security":[{"opsBasic":[]}],"responses":{"200":{"description":"Audit event stream","content":{"application/x-ndjson":{"schema":{"type":"string"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/game/{id}.jsonl":{"get":{"tags":["ops"],"summary":"Authenticated replayable room action log","description":"Seed plus the ordered action stream for one tank — the ISO/IEC 42001 A.6.2.8 event record. Also served at /audit/game/{id}.jsonl.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"security":[{"opsBasic":[]}],"responses":{"200":{"description":"Room actions with internal replay identifiers","content":{"application/x-ndjson":{"schema":{"type":"string"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/replay/{id}":{"get":{"tags":["ops"],"summary":"Reconstruct a room at a retained tick","description":"Deterministic reconstruction of authoritative state, including every autonomous agent, at a chosen tick. Also served at /audit/replay/{id}.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"tick","in":"query","required":false,"schema":{"type":"integer","maximum":100000}}],"security":[{"opsBasic":[]}],"responses":{"200":{"description":"Reconstructed authoritative state","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Operations authentication required"},"410":{"description":"Complete replay history expired"}}}},"/admin/status.json":{"get":{"tags":["ops"],"summary":"Private operational status and billing data","description":"The unredacted operational record, including the running version identifier and instance residency. Also served at /audit/status.json.","security":[{"opsBasic":[]}],"responses":{"200":{"description":"Full status, audit counters, and billing window","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/maintenance":{"post":{"tags":["ops"],"summary":"Enable or disable maintenance mode","description":"Same-origin Admin action. Enabling creates a separate operator-maintenance incident, closes active WebSockets, and returns the controlled 503 page for the game shell, game assets, and tank traffic while Roadmap, API, Docs, Status, Incidents, Inquiry, Logs, the conformance register, and Admin remain online. Disabling records the end of service impact but leaves independent security reports active until separately resolved. Every transition is persisted to the action log and the control-history receipt chain.","security":[{"opsBasic":[]}],"parameters":[{"name":"X-WG-Ops-Action","in":"header","required":true,"schema":{"type":"string","enum":["maintenance"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Maintenance state","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/billing-reset":{"post":{"tags":["ops"],"summary":"Reset the billing measurement window","description":"Resets measured billing baselines without resetting service uptime or status history.","security":[{"opsBasic":[]}],"parameters":[{"name":"X-WG-Ops-Action","in":"header","required":true,"schema":{"type":"string","enum":["billing-reset"]}}],"responses":{"200":{"description":"Fresh billing window","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Operations authentication required"}}}},"/admin/security-resolve":{"post":{"tags":["ops"],"summary":"Resolve owner-confirmed security exercises","description":"Resolves open white-hat reports as owner-confirmed dry runs without changing the separate maintenance gate. Appends one immutable resolution receipt per report.","security":[{"opsBasic":[]}],"parameters":[{"name":"X-WG-Ops-Action","in":"header","required":true,"schema":{"type":"string","enum":["security-resolve"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ownerConfirmed":{"type":"boolean","enum":[true]},"dryRun":{"type":"boolean","enum":[true]},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Owner-confirmed resolution receipts","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Owner confirmation and dry-run flag required"},"401":{"description":"Operations authentication required"}}}},"/admin/security-report":{"post":{"tags":["ops"],"summary":"File a security report and take the game down","description":"Same-origin Admin action. Records the same report, audit event, and control-history receipt as the public intake, and additionally opens an active security incident, enables game maintenance, and disconnects active tanks pending operator review. Restoring game traffic records the end of service impact but does not resolve or close the security report; that is /admin/security-resolve. At most one security-report lockdown is open at a time — a repeat call while one is open returns the existing incident and creates no second incident or receipt. Roadmap, Status, Incidents, Inquiry, Logs, Docs, API, the conformance register, and authenticated Admin remain available throughout.","security":[{"opsBasic":[]}],"parameters":[{"name":"X-WG-Ops-Action","in":"header","required":true,"schema":{"type":"string","enum":["security-report"]}}],"responses":{"200":{"description":"Linked lockdown and report receipt","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Operations authentication required"},"403":{"description":"Same-origin operation required"},"502":{"description":"Report and lockdown could not be persisted"}}}},"/admin/test-alert":{"post":{"tags":["ops"],"summary":"Send an authenticated test alert","description":"Accepts exactly one ASCII letter followed by three digits, normalizes the letter uppercase, and records the acknowledgement in the action log.","security":[{"opsBasic":[]}],"parameters":[{"name":"X-WG-Ops-Action","in":"header","required":true,"schema":{"type":"string","enum":["test-alert"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[A-Za-z][0-9]{3}$","minLength":4,"maxLength":4}}}}}},"responses":{"200":{"description":"Test alert receipt","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Code does not match letter-digit-digit-digit"},"401":{"description":"Operations authentication required"}}}}},"components":{"securitySchemes":{"opsBasic":{"type":"http","scheme":"basic","description":"Any username; OPS_TOKEN is the password."}},"schemas":{"HealthResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"module":{"type":"string"},"time":{"type":"string","format":"date-time"}}},"TankRoom":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"players":{"type":"integer"},"bots":{"type":"integer"},"capacity":{"type":"integer"},"topScore":{"type":"integer"},"topName":{"type":"string"}}},"TankResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"rooms":{"type":"array","items":{"$ref":"#/components/schemas/TankRoom"}}}},"ScoreEntry":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"skin":{"type":"string"},"score":{"type":"integer"},"alive":{"type":"boolean"}}},"LeaderboardResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"entries":{"type":"array","items":{"$ref":"#/components/schemas/ScoreEntry"}}}},"Profile":{"type":"object","properties":{"name":{"type":"string"},"skin":{"type":"string"},"best":{"type":"integer"},"settings":{"type":"object","additionalProperties":true}}},"ProfileResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"profile":{"$ref":"#/components/schemas/Profile"}}},"AuditEvent":{"type":"object","properties":{"ts":{"type":"integer","description":"Unix ms timestamp"},"type":{"type":"string","enum":["room-boot","join","leave","death","play","customize","skin","settings","nav","quit","maintenance-on","maintenance-off","billing-reset","billing-hard-stop","security-report","security-resolved","test-alert"]},"room":{"type":"string"},"subject":{"type":"string","description":"Player/shark name"},"detail":{"type":"string"}}},"ControlHistoryEntry":{"type":"object","properties":{"sequence":{"type":"integer","description":"Monotonic receipt sequence"},"ts":{"type":"integer","description":"Unix ms timestamp"},"code":{"type":"string","description":"Stable control code"},"actor":{"type":"string","description":"Control actor class"},"title":{"type":"string"},"summary":{"type":"string"},"reference":{"type":"string","description":"Linked report, incident, alert, or billing-window id"},"detail":{"type":"string"},"previousHash":{"type":"string","description":"Previous SHA-256 receipt"},"hash":{"type":"string","description":"SHA-256 receipt for this entry"}}},"Usage":{"type":"object","properties":{"startedAt":{"type":"integer"},"uptimeMs":{"type":"integer"},"presenceReports":{"type":"integer"},"durableObjects":{"type":"object","properties":{"tank":{"type":"integer"},"rooms":{"type":"integer"},"total":{"type":"integer"}}}}},"Availability":{"type":"object","properties":{"windowHours":{"type":"integer"},"uptimeMs":{"type":"integer"},"downtimeMs":{"type":"integer","description":"Alias for unscheduled downtime"},"scheduledDowntimeMs":{"type":"integer"},"unscheduledDowntimeMs":{"type":"integer"},"availabilityPercent":{"type":"number"},"scheduledDowntimePercent":{"type":"number"},"unscheduledDowntimePercent":{"type":"number"},"calculatedAt":{"type":"string","format":"date-time"}}},"StatusResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"maintenance":{"type":"object","properties":{"enabled":{"type":"boolean"},"changedAt":{"type":"integer"},"reason":{"type":"string"}}},"usage":{"$ref":"#/components/schemas/Usage"},"history":{"type":"array","items":{"$ref":"#/components/schemas/ControlHistoryEntry"}},"historyIntegrity":{"type":"object","properties":{"mode":{"type":"string"},"algorithm":{"type":"string"},"entryCount":{"type":"integer"},"headHash":{"type":"string"}}},"rooms":{"type":"array","items":{"$ref":"#/components/schemas/TankRoom"}},"global":{"type":"array","items":{"$ref":"#/components/schemas/ScoreEntry"}},"portalAvailability":{"$ref":"#/components/schemas/Availability"},"tankAvailability":{"$ref":"#/components/schemas/Availability"}}},"ErrorResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":false},"error":{"type":"string"}}}}}}