After 4 rounds + ~165 tests + 13 product bugs fixed, round 5 hits the last bedrock classes: multi-tenant data isolation across listing endpoints, scoresheet co-sign code brute-force resistance, live-scoring access control, JSON-payload DoS, injury substitution rule path, and admin mass-action authorization.
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Public stats — hkpl vs uat returns DIFFERENT data (tenant routing works) | ✓ Different per host | hkpl=200 uat=200 hkpl_size=56 uat_size=56 |
| ✓ | Fetch uat team stats via hkpl host | ✓ 404 (tenant-scoped) | http=404 |
| ✓ | Fetch hkpl team stats via uat host | ✓ 404 (tenant-scoped) | http=404 |
| ✓ | /public/teams under hkpl host — only hkpl-tenant ids | ✓ Pure hkpl | count=126 sample_all_hkpl=true leaked_uat_id=false |
| ✓ | /public/teams under uat host — only uat-tenant ids | ✓ Pure uat | count=141 sample_all_uat=true leaked_hkpl_id=false |
| ✓ | /public/results under uat host — sample IDs all uat-tenant | ✓ Pure uat | sample_ids=5 all_uat=true |
| ✓ | uat captain cookie reused on hkpl host — captain API access | ✓ Blocked | http=401 |
| ✓ | SUPER_ADMIN sees all tenants via /super/tenants | ✓ 200 (by design) | http=200 count=6 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Submit a real scoresheet to create the per-game match codes | ✓ Submitted | http=200 |
| ✓ | Per-game code entropy (3 random bytes → 6 hex chars) | ✓ 16^6 = 16.7M combinations × 5 games | crypto.randomBytes(3).toString('hex').toUpperCase() |
| ✓ | Wrong 6-char code (AAAAAA) rejected | ✓ Blocked | http=403 {"error":"invalid_match_code"} |
| ✓ | Brute-force code flood (20 wrong codes) hits 429 | ✓ Rate-limited | codes=403,429 429=16 |
| ✓ | Empty body — match_code missing | ✓ 400 | http=400 {"error":"match_code_required"} |
| ✓ | Non-captain (PLAYER) tries to sign | ✓ Blocked | http=403 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Judge starts live scoring | ✓ 200 | http=200 {"ok":true,"match_id":4202,"live":{"state":"in_progress","teamA":0,"teamB":0,"current_game":1,"games":[{"a":0,"b":0}],"format":"BO5-21-SUDDEN-DEATH-AT-20","max_games":5,"score_targ |
| ✓ | Judge logs a point on team A | ✓ 200 | http=200 {"ok":true,"live":{"games":[{"a":1,"b":0}],"state":"in_progress","teamA":1,"teamB":0,"events":[{"t":"s","by":"ee960932-ea93-4b3b-8a6a-beac6b7a7c70","ts":1782301319222,"format":"BO5 |
| ✓ | Judge undo last point | ✓ 200 | http=200 |
| ✓ | Random PLAYER tries to log a live point | ✓ Blocked (403) | http=403 |
| ✓ | Spectator tries to log a live point | ✓ Blocked | http=403 |
| ✓ | Malformed point payload (team="INVALID") | ✓ Rejected | http=400 {"error":"team_required_A_or_B"} |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | 11MB body — rejected or dropped, server stays up | ✓ http=0, health 200 after | payload_resp=0 health_after=200 |
| ✓ | 100k-element array — server stays up | ✓ health 200 after | payload_resp=0 health_after=200 |
| ✓ | Prototype pollution attempt via __proto__ / constructor.prototype | ✓ Object.prototype intact | pollute_attempt_http=404 polluted=false |
| ✓ | 2000-deep nested object — server doesn't crash | ✓ Returned 404 | http=404 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Judge files an injury sub-request | ✓ Filed | http=200 {"ok":true,"kind":"queued_for_review","request_id":"cmqs06rqy000ro0u1b67uuhez","message":"Substitution request filed. A division rep will review and approve."} |
| ✓ | Random PLAYER tries to file a sub-request | ✓ Blocked | http=403 |
| ✓ | Sub-request with missing fields | ✓ 400 | http=400 {"error":"invalid_body","detail":[{"code":"invalid_type","expected":"string","received":"undefined","path":["original_dupr"],"message":"Required"},{"c |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Captain tries to broadcast a notification | ✓ Blocked (403) | http=403 |
| ✓ | Admin sends broadcast to PLAYER role | ✓ 200 | http=200 {"ok":true,"recipients":1730,"target_role":"PLAYER"} |
| ✓ | Admin broadcast with empty body | ✓ Rejected | http=400 {"error":"title_body_target_role_required"} |