VOIDERATION V24: PLAYFAB BANS + BADGUYSCENE INTEGRATION Date: 2026-10-30 Status: Research/planning only. No code, revision, API, dashboard or production changes. Additive update to V22/V23; those reports remain unchanged. DECISION / CURRENT FACTS PlayFab is authoritative for ban state. Voideration dashboard is the owner control surface; Worker is the only holder of PlayFab Title Secret Key and performs Admin ban/query/revoke operations. D1 stores Voideration incident and action history plus a reconciled projection of PlayFab results, not the final authority for whether a PlayFab ban is active. CONFIRMED from local source/reports: - Unity `Oculus_Login` uses client-obtained Meta ID as PlayFab CustomId; successful login returns canonical PlayFabId and SessionTicket. Meta ID is not proof. - `HandleBan` can display ban reason/expiry using configured UI, but does not load BadGuyScene. `KickIfBanned` separately polls account info then disconnects and loads scene index 1 on `AccountBanned`. No `BadGuyScene` was found. - Current Photon auth passes PlayFabId as username and the distinct PlayFab Photon token as token; a 20-second anonymous fallback exists. Neither the browser nor Unity may receive PlayFab Title Secret Key. - Worker `/redeem` validates a PlayFab SessionTicket and derives PlayFabId for link flow. Existing owner Discord ban commands are not a sufficient dashboard ban system: KV keeps a fragile single BanId; unban relies on it. Worker source must verify the current PlayFab revoke route against documented `/Admin/RevokeBans`. - Worker/D1 dashboard, ban API, BadGuyScene and reliable live PUN-session kick are not implemented. No dashboard/provider settings were inspected. TARGET ARCHITECTURE / TRUST BOUNDARIES Local owner website (password every opening; 127.0.0.1 only) | owner-authenticated Worker API over TLS; no secrets in JS v Cloudflare Worker -- PlayFab Admin APIs (Title Secret Key server-only) | | BanUsers/GetUserBans/RevokeBans | v | PlayFab = authoritative ban record +-- D1 = incident/action audit + PlayFab BanId/result mirror +-- trusted ban-status endpoint -> Unity BadGuyScene +-- Photon custom-auth gate checks validated identity + effective ban Owner actions require backend authentication/authorization every request. Never trust a PlayFabId, Discord username, ban status, duration or reviewer supplied by game client. Never put Title Secret Key in browser JS, Unity, logs, URLs or D1. The local site remains primary; optional Discord notification cannot authorize, apply, or reverse a ban. OWNER DASHBOARD AND STATE TRANSITIONS Incident state and PlayFab ban state are separate: - Detection -> incident `PendingReview`; it does not itself ban or block rejoin unless an explicitly approved temporary review restriction applies. - `Approve`: mark incident Approved, clear PendingReview restriction and allow rejoining after the trusted Photon admission check observes release. - `Dismiss`: mark incident Dismissed/closed (or `Rejected` in the V22-compatible schema with a false-positive reason), no ban, release only a PendingReview hold. This is the false-positive close action. - `Deny/Ban`: owner chooses reason and duration (finite duration or permanent option if PlayFab API supports it), re-authenticates, confirms. Worker calls PlayFab Admin BanUsers. On success store PlayFab BanId(s), returned expiry, reason, actor, UTC time and result in D1; incident history links to the ban. Only after confirmed PlayFab success display `Banned`. On failure/timeout show pending/uncertain, not success; reconcile via GetUserBans before retry. - `Release/Revoke`: explicit owner action calls PlayFab RevokeBans for recorded active BanId(s), records response and updates mirror after provider confirms. Do not use RevokeAllBansForUser unless owner explicitly chooses broader effect. PlayFab GetUserBans may return inactive/expired records with `Active=true`; treat ban as effective only when it is not revoked and `Expires` is absent (permanent) or later than current UTC. Retain all BanIds; multiple bans are possible. Finite duration arithmetic must use UTC and API limits. “Very long” timed ban is not a permanent ban. Verify exact Admin API semantics and current Worker route before implementation. Worker ban operation should persist a unique operation/idempotency key and intent, call PlayFab, then persist provider response. If PlayFab succeeds but D1 update fails, operation is uncertain: query GetUserBans by trusted PlayFabId, reconcile BanIds and do not issue a duplicate ban blindly. If PlayFab fails, store failure and do not mark banned. D1 is an audit/projection; PlayFab remains source of truth. Audit successful and denied owner actions, reasons, before/after, request ID and provider result. Require owner reauthentication for ban/revoke. BADGUYSCENE AND BAN STATUS WHEN LOGIN IS DENIED Unity should enter `BadGuyScene` only on a definitive `AccountBanned` response or a trusted Worker response that independently confirms an effective PlayFab ban. Display sanitized reason, permanent vs remaining duration, and absolute UTC expiration where finite. Missing/malformed reason/expiry gets safe generic text; do not parse provider text as executable markup. Network, title, account, or Worker errors are not bans and must use normal retry/error UI. Do not join Photon while ban status is unresolved or active. Preserve existing unbanned login, inventory, title data, MOTD, rooms, voice and scene flow; avoid scene-index-only assumptions and test return/navigation behavior. Secure lookup options, in preference order: 1. Use authoritative PlayFab `AccountBanned` error details returned by login if they reliably contain reason/expiry; test current SDK/provider behavior. These details may be absent, so sanitize and handle optional values. 2. If a trusted lookup is needed after login rejection, Unity obtains a fresh Meta User Proof for a server-issued one-use nonce. Worker verifies proof and identity with Meta server-to-server, checks the verified Meta-to-PlayFab account binding, then calls PlayFab GetUserBans and returns only minimal ban display fields. Worker derives PlayFabId from the verified mapping; client never sends a trusted PlayFabId. Proof nonce is single-use, short-lived and never logged/stored long-term. 3. If Meta verification/eligibility, proof exchange, or account binding is not available, do not create a public lookup taking client PlayFabId or username. Show generic banned/login failure when provider confirms ban but details are unavailable. Do not claim account-specific display lookup works until this path is proven in staging. Meta User Verification is a possible identity bridge, not established eligibility or current implementation. Current Meta ID is client supplied and insufficient. For existing accounts, require verified mapping to their PlayFabId; do not assume Meta ID equals PlayFabId or silently create a new account. An authenticated PlayFab SessionTicket is another trusted identity input when available, but the normal ban-rejected login may not yield a usable ticket. Photon token is not a PlayFab SessionTicket and must not be passed to AuthenticateSessionTicket. PHOTON SESSION DISCONNECT / RECONNECT ENFORCEMENT - PlayFab BanUsers is authoritative for PlayFab account access but does not by itself prove that an already-connected Photon PUN Cloud actor is immediately removed. Current research found no reliable targeted-disconnect API available to an ordinary Worker request. A cooperative client can receive a ban notice and disconnect itself, but a modified client may ignore it. Do not use room closure/MasterClient actions that disrupt others; describe existing session as connected until observed disconnect. - On every new Photon custom-auth/reconnect, trusted server-side admission must validate the Photon credential with the supported PlayFab/Photon contract, derive canonical PlayFabId, query effective PlayFab ban and D1 temporary restriction, and deny if either active. Do not trust username. The exact Worker-to-PlayFab Photon proxy contract remains unverified; do not guess. - Close all Unity anonymous Connect fallbacks and configure Photon to reject anonymous clients before claiming bans prevent reconnect. Test app restart, room rejoin, voice app/auth and token-refresh/reconnect. Photon custom auth is connection-time, not live-session enforcement. - During Worker/PlayFab/D1 outage, return retryable auth-service failure with no anonymous fallback. Do not report a new ban or change ban state on outage. Existing sessions are not mass-kicked. If ban lookup is unavailable after a rejected login, show retryable status; do not label user permanently banned. LINKED DISCORD IDENTITY Dashboard may show linked Discord account only when Worker verifies an existing link record created by the authenticated one-time code flow and associates it with the canonical PlayFabId. Treat it as verified link metadata, not identity proof for login, ban lookup or permission. Never display a Discord username received directly from client; resolve the stored Discord user ID server-side through trusted Discord data if a current username is desired. Clearly label unlinked/unknown; minimize Discord data and access. Audit link/unlink changes. DASHBOARD ACCESS / FAILURE HANDLING Website is primary and bound only to `127.0.0.1:`. Require password on every opening (fresh login; no remembered browser session), rate-limit attempts, store only a strong salted password verifier server-side, use secure short-lived session cookie, CSRF + strict Host/Origin checks, and require fresh password confirmation for Ban/Revoke. Backend rechecks owner permission. Use OS-protected local credential storage for Worker authentication; no secrets in JS/localStorage. Local machine compromise remains a trust boundary. UI failure/offline must not optimistically apply a ban; show pending/failed/uncertain state and reconcile. Optional Discord critical alert follows provider confirmation; dashboard remains usable if Discord is offline. DEPENDENCIES / TESTS 1. Verify PlayFab Admin BanUsers/GetUserBans/RevokeBans API contracts, permanent vs timed semantics, required fields and error/idempotency behavior; inspect deployed Worker and secret binding names without exposing values. Reconcile existing Discord ban/unban commands and old BanId KV records before migration. 2. Decide owner duration choices, reason policy, appeal/release process, password provisioning/recovery, privacy/retention, and whether Meta proof is mandatory for denied-login ban details. Default: owner-only, finite durations by default, permanent requires explicit confirmation; dismissal never bans. 3. Stage Worker/D1 ban mirror and reconciliation, then dashboard actions. Test PlayFab success + D1 failure, provider timeout after success, duplicate clicks, multiple BanIds, expired/revoked bans, revoke one vs all, and audit completeness. 4. Validate login errors and `BadGuyScene`: timed/permanent ban, missing details, malformed expiry, ordinary network/title/auth failure, Worker unavailable, Meta proof replay/wrong user, unmapped legacy account. No client PlayFabId spoofing; no ban display for non-ban errors. 5. Photon tests: active ban denied on fresh connect/rejoin/app restart; revoke permits rejoin; temporary V22 review hold releases on Approve; no anonymous fallback/admission; Voice and ordinary rooms still work. Confirm existing connected player is not falsely reported as kicked and other room users remain. 6. Regression: preserve Oculus/PCDEBUG login, PlayFab inventory/currency and purchases, `/link`, MOTD, CCU, Tag/Infection, PVP, movement, Sandbox/welding, horror hub/LVL1-7, voice, scene transitions and reconnects. Read-only sources reused: V15/V17/V19/V21/V22/V23, local `Oculus_Login.cs`, `PhotonVRManager.cs`, `worker.js`, `/redeem` session-ticket validation, and existing ban command paths. PlayFab Admin API contracts and Meta eligibility must be confirmed against current provider documentation and staging before implementation. No dashboards or production services were accessed. END OF REPORT