Fighting Game · Architecture

Security, Compliance & Sister-Monorepo Integration

A focused page within the Fighting Game Architecture documentation. The full map and every sibling page live in the Architecture hub.

6sections12 minread2diagrams

On this page

V2 is a globally-shipped, age-rated, real-money-transacting competitive game, so its security and compliance surface is cert-blocking — a gap here is not a backlog item, it is a reason a platform holder refuses to ship. The defining architectural choice is that V2 does not reinvent any of it: identity, PII handling, data residency, consent, age-gating, audit, and data-subject-rights are all delegated to existing Oshun, Aphrodite, Themis, and Nous packages, reached through thin V2 service surfaces under apps/v2/. That composition — V2 binding to its sister monorepos rather than forking their logic — is the "sister-monorepo integration" in the title, and it is precisely what lets a deletion request, a ban, or a likeness revocation resolve consistently across every Oshun product instead of just this one. The same instinct runs through the engine: the deterministic, rollback-synchronized combat loop stays sovereign-local, while everything non-deterministic (the "warm path") leans on shared infrastructure. This page covers the security/privacy posture, the data-subject-rights architecture, and the full integration surface that wires V2 to the monolith. The section hub is ../V2_ARCHITECTURE.md.

What ships, honestly#

The integration packages are real, and several are substantively implemented. apps/v2/ holds 91 packages; every @v2/* compliance surface this page cites is one of them and composes the corresponding shared contract. @v2/oshun-identity-binding (apps/v2/oshun-identity-binding/src/oshun-identity-binding.ts:49) wraps @oshun/identity; @v2/aphrodite-age-gate and @v2/aphrodite-consent-surfaces wrap @aphrodite/age-verification and @aphrodite/consent-engine; @v2/themis-privacy-dsr-routing joins @themis/privacy with @oshun/data-residency and @oshun/audit-platform; and @v2/nous-anti-cheat-classifiers composes @nous/training + @nous/safety. The strongest anchor is @v2/telemetry-privacy-compliance (telemetry-privacy-compliance.ts), which is not a thin wrapper at all — it carries a real 33-entry GDPR region-code set (:177), a CCPA US-CA branch (:355), a per-data-class retention policy (operational 7d / optional 30d / behavioral 14d, :250), and a PII-field redactor that drops forbidden fields at ingest (:315).

The save-header identity fields are real engine state. FV2SaveHeader (V2/ue/Source/V2Persistence/Public/V2PersistenceTypes.h:242) stores OshunAccountId (:253), PlatformUserId (:256), and HomeRegionId (:259) as three separate fields, and the save validator actively refuses to load a slot whose account does not match the header (V2/ue/Source/V2Persistence/Private/V2SaveGame.cpp:39,51,65). The legal data is checked-in, not just prose: V2/legal/rating-boards.json, V2/legal/sub-processors.md, and V2/legal/platform-cert-bans.json are real files, and the cert-ban truth table is enforced by a real CI gate at V2/ue/Tools/check-platform-cert-bans.py (region rules by check-v2-region-rules.py).

What is policy rather than code, labelled as such throughout: the per-region datacenter routing (Frankfurt/Stockholm, Tokyo, Seoul, São Paulo, CN publisher cloud) is residency infrastructure these packages assume, not infrastructure they provision; the runtime gRPC clients the integration table promises (libs/proto/v2/online/) are a designed wire contract whose proto package is not in the tree (see Online Backbone & Competitive Integrity); and some named cert artifacts — cook-region.py, check-cn-content-rules.py, V2/legal/cert-windows.md — are not yet present and are honest "planned" items. As ever: the enforcement seams are coded, the obligations are documented and audited.

Security: identity, sessions & the save-header invariant#

Identity roots in the platform, not in V2. @oshun/identity is the single account root; per-platform shadow accounts (PSN, Xbox Live, Nintendo Account, Steam) bind to it through the V2 @v2/oshun-identity-binding surface, which validates that every platform account binds to exactly one Oshun account before it returns (oshun-identity-binding.ts:54). The engine keeps these separable on purpose. Because FV2SaveHeader carries OshunAccountId and HomeRegionId distinctly from PlatformUserId, and the header even stamps its provenance (IdentitySourcePackageName = "@oshun/identity", V2PersistenceTypes.h:262), a cloud save can never silently merge two identities — the cross-field equality checks in V2SaveGame.cpp are the mechanism, not a comment. This is the load-bearing security invariant of cross-progression: shared profile, Crowns ledger, cosmetics, and ranked rank all hang off one root, so revoking that root revokes everywhere at once.

Sensitive actions are gated, not assumed. The binding surface exposes an accountMergeGate and a platformRebindGate (oshun-identity-binding.ts:71-72) and a regionChangeDecision (:73) — the TOTP-stepped, cool-off-bounded checks the monolith requires for account merge, payment-detail change, and ranked-pro qualification. Session security itself is conventional and strict: short-lived JWTs (15 min) with rotating 30-day refresh tokens and cross-region session sync, the same foundation documented on the platform Auth & Identity page. Critically, the binding surface is marked offRollback: true / mayInfluenceRollback: false (:78-79) — identity authority is in the warm path and can never perturb a deterministic frame.

V2 handles PII by stripping at ingest rather than trusting downstream deletion, and @v2/telemetry-privacy-compliance is where that actually happens. applyTelemetryPrivacyComplianceToBatch (telemetry-privacy-compliance.ts:262) walks each telemetry event, redacts any forbidden field (device_ad_id, full_name, phone_number, plus the inherited V2_TELEMETRY_FORBIDDEN_PAYLOAD_FIELDS) before the event is accepted (:315), classifies the event's data class, and then runs a genuine legal-basis decision (evaluateTelemetryEventAllowance, :364): required operational telemetry rides "contract-or-legal-obligation" with 7-day retention; optional and behavioral telemetry require a consent basis; a GDPR-region player with no explicit opt-in has optional telemetry rejected (:400); and a CCPA Do-Not-Sell/Share opt-out suppresses behavioral telemetry outright (:387). Raw IP, device fingerprint, and real-name fields therefore never reach the analytics warehouse — by code, per-event, with an audit topic emitted when a redaction fires (:470).

Per-feature consent is likewise not hand-rolled. @v2/aphrodite-consent-surfaces (aphrodite-consent-surfaces.ts:45) composes @aphrodite/consent-engine to govern telemetry, voice-processing, and behavioural-profiling consent; it requires consent receipts when a feature is enabled, defaults missing opt-ins to privacy-preserving fallbacks, and stays off rollback. Age-gating runs through @v2/aphrodite-age-gate (aphrodite-age-gate.ts:52) over @aphrodite/age-verification as a backend-only driver for gore tier, adult fatalities, and region-conditional cinematics. Two honesty details are encoded directly in the surface: aphroditeBrandHiddenFromPlayers: true (the adult-platform brand never shows) and record2257Required: false / antiCsamPerformerWorkflowRequired: false (:42-44) — the audit explicitly applies the mature-rated-fighting-game profile and rejects Aphrodite's adult-content default, so performer-protection workflows meant for adult content are excluded from game-content gating.

Region content rules are real shared data, not prose: @oshun/region-rules (libs/shared/region-rules/src/v2-regional-content-rules.ts:5) enumerates the CN/DE/AU/NZ/KR/GLOBAL country axis, the gore-tier ladder (none/mild/standard/extreme), finisher policies, and the per-region content cuts (remove-blood, recolor-red-blood, disable-adult-fatalities, …) that the checked-in V2/legal/rating-boards.json matrix (ESRB M, PEGI 18, CERO Z, USK 18, ACB R18+, GRAC, ClassInd 18, OFLC R18, with SKU tags and streaming-patch downgrades) maps onto. Missing region data defaults to the China most-restrictive profile. The per-region cook itself (cook-region.py) and the CN content gate (check-cn-content-rules.py) are the planned-but-absent half of this story.

Compliance & the data-subject-rights architecture#

Data-subject rights compose three packages with a clean separation of duties. @v2/themis-privacy-dsr-routing (themis-privacy-dsr-routing.ts:91) joins @themis/privacy — which owns the access / erasure / portability / rectification workflow and the regulatory SLA — with @oshun/data-residency, which owns the subject-home route, and @oshun/audit-platform, which owns the retained evidence. The SLAs are real, frozen constants, not aspirations: SLA_DAYS_BY_FRAMEWORK (libs/themis/privacy/src/v2-data-subject-rights.ts:155) pins gdpr: 30, uk_gdpr: 30, ccpa_cpra: 45, lgpd: 15, pipeda: 30, and a global_privacy_baseline: 30. The workflow builder also fails loud on a missing 2FA assertion (:172) and on a rectification request with no fields (:179) — a DSR cannot be routed without authenticating the subject first.

flowchart TB Player[V2 player] --> Form[In-game DSR form] Form --> TwoFA[Account 2FA · required, fails loud] TwoFA --> V2DSR["@v2/themis-privacy-dsr-routing"] V2DSR --> Themis["@themis/privacy<br/>workflow + SLA (30/45/15d)"] V2DSR --> Res["@oshun/data-residency<br/>createDsrResidencyRoutingDecision"] Res --> Queue["themis.privacy.dsr.&lt;homeZone&gt;<br/>standaloneConsole = false"] V2DSR --> Audit["@oshun/audit-platform<br/>buildV2AuditPublicationRequest"] Themis --> Access[Access JSON/CSV] Themis --> Erasure[Erasure + anon aggregates] Erasure -->|cascades| Shadow[PSN · Xbox · Nintendo · Steam shadow accounts] Themis --> Port[Portability] Themis --> Rect[Rectification]

The residency half is equally concrete. createDsrResidencyRoutingDecision (libs/shared/data-residency/src/dsr-routing.ts:48) resolves the subject's home zone from the JWT claim, produces a zone-scoped queue name under the themis.privacy.dsr prefix (:13), attaches the cross-region-safeguard flag, and hard-codes standaloneDsrOpsConsoleAllowed: false (:46). The V2 surface re-asserts that as standaloneDsrOpsConsoleReplaced: true (themis-privacy-dsr-routing.ts:165): V2 ships no bespoke deletion console — a V2 player's request flows into V1's existing, home-zone-routed pipeline. Erasure is explicitly scoped (account, profile, replay_anonymization, telemetry_purge, cosmetic_ledger, :22) and produces a documented three-step audit trail (request → action → confirmation) with anonymized retention (:171), preserving anonymized aggregates per GDPR Recital 26. Because identity binds through one root, that erasure cascades to every bound shadow account — the cross-progression invariant paying off again.

Every moderation, anti-cheat, and DSR action publishes a canonical event through @oshun/audit-platform. The shared module (libs/shared/audit-platform/src/v2-audit-publication.ts) defines exactly three publication kinds — moderation, anti_cheat, dsr (:12) — each with a retention tag, and buildV2AuditPublicationRequest (:89) is invoked literally by the DSR surface (themis-privacy-dsr-routing.ts:137) and the anti-cheat surface (nous-anti-cheat-classifiers.ts:112). V2 produces the evidence; Oshun retains and exports the investigation-ready trail. The anti-cheat ML suite belongs to this compliance posture too: @v2/nous-anti-cheat-classifiers (nous-anti-cheat-classifiers.ts:78) is governed by a Model Card and pins automatedDisciplineAllowed: false, humanReviewRequired: true, appealsRequired: true (:59-61), with a validateSuite guard (:143) that refuses any configuration able to influence rollback. AI-system transparency is its own surface: @v2/eu-ai-act-surface (eu-ai-act-surface.ts:30) registers the four shipping AI services (adaptive-AI-director, AI-commentary, anti-cheat-classifier-suite, generation-pipelines) against @nous/safety Model Cards and @themis/accountability AI-system-of-record exports — the EU AI Act conformity seam. DSA Statement-of-Reasons publication and appeals route out to Themis (@v2/themis-dispute-resolution), keeping discipline auditable and reversible by the same machinery that governs every Oshun product.

Sub-processor governance#

Compliance is not only about player-facing flows; it is also about who else touches the data. V2/legal/sub-processors.md is a real, dated registry naming the twelve production sub-processors V2 routes data to — the four platform holders (Sony, Microsoft, Nintendo, Valve) plus Epic, the cloud tier (AWS for regional hosting/telemetry/backups, Azure for failover/security analytics, Google Cloud for aggregate analytics isolation), Cloudflare (CDN/DDoS/WAF), Stripe (PC/web payments, no raw card storage by V2), Zendesk (support + DSR intake), and Sentry (crash triage with scrubbed identifiers). Each row carries a contract reference (DPA-CLOUD-AWS-V2-2026, …), a jurisdiction, and the exact data categories accessed, and the document encodes the governing rules: a new production sub-processor needs DPO/legal approval and 30 days' player notice, per-platform DPAs are required before launch, and privacy legal reviews the registry quarterly and before each launch region opens. The AWS row makes the residency story concrete — "account, gameplay, telemetry, replay, moderation, audit, backup data by residency zone" — which is exactly the zone partitioning the @oshun/data-residency decisions above assume but do not themselves provision.

The sister-monorepo integration surface#

The integration is governed by one rule: the deterministic inner loop is sovereign, everything else is delegated. The sovereign-local set is non-negotiable because it sits inside the frame-deterministic, rollback- synchronized, or cert-driven envelope — the hitbox/hurtbox engine, frame-data runtime, motion-input parser, rollback engine, save-format binary + replay container, Sequencer timelines, per-platform IMC files, and the anti-cheat client driver. Everything outside that envelope is in scope for delegation, and V2 consumes the monolith across three channels:

flowchart LR subgraph V2local[Sovereign V2 — frame-deterministic] Combat[V2Combat · Netcode · Input · Save/Replay] end subgraph ChanA[Channel A · build-time, offline] Bellona["@bellona/unreal cook"] end subgraph ChanB[Channel B · runtime warm path, off-rollback] Bus["@oshun/event-bus + gRPC (designed)"] end subgraph ChanC[Channel C · out-of-game] Shell["apps/oshun/web + mobile shell"] end Combat -. emits replay/match events .-> Bus Isis[Isis · Hathor · Euterpe · Aja] --> Bellona --> Combat Bus --> Maat[Maat balance] & Iris[Iris commentary] & Themis2[Themis appeals] Shell --> Companion[Companion · public API · wiki]

Channel A — build-time content pipeline (offline). Bellona orchestrates Isis-generated assets, Aja-cleaned mocap, Hathor-authored narrative, Euterpe-generated music, and Themis-cleared rights manifests into engine-native .uasset / .umap artifacts through the existing @bellona/unreal bridge; V2's cook consumes the output. The pattern for bulk generated content is uniform: Isis generates → Themis verifies originality → Kuanyin moderates → Bellona cooks → V2 consumes.

Channel B — runtime services bus (warm path, off-rollback). apps/v2/ is a thin adapter layer over the Oshun @oshun/event-bus (Redis Streams) and — as designed — gRPC clients generated from libs/proto/. V2 publishes lifecycle events (v2.match.ended, v2.cosmetic.purchased, v2.player.reported) and subscribes to ecosystem events (maat.balance.recommendation, hathor.dialogue.ready, isis.asset.cooked). This is the channel the per-surface mapping table fills in: ML balance to Maat (@v2/maat-balance-dashboard), commentary to Iris/Calliope/Psyche, moderation to Kuanyin/Themis, NPC AI to Hathor (@v2/hathor-npc-adapter), and compliance to the shared residency/age/consent packages above. The honest caveat stays attached: the proto package is not yet in the tree, so the gRPC method signatures are the designed contract, while the event-bus substrate is the real, shipped one.

Channel C — out-of-game surfaces. The marketing tile, companion app, public API, frame-data wiki, and esports portal become tiles under apps/oshun/web (/v2/...) and apps/oshun/mobile/v2/, consuming the @oshun/shell-core umbrella plus specific surface clients (@oshun/auth-client, @oshun/trust-safety, @iris/agents, @sophia/client). There is deliberately no monolithic @oshun/sdk; consumers import the narrow surfaces they need. Cross-product entitlements (a Lilith meditation streak unlocking a V2 card border) become trivial because account state already lives in @oshun/identity. Optional Web3 cosmetic ownership (@v2/aje-web3-cosmetic-ownership) and the racing-ecosystem bridge (@v2/racing-ecosystem-bridge) ride the same off-rollback discipline, each cert-gated by V2/legal/platform-cert-bans.json and the real check-platform-cert-bans.py gate.

Edge cases & failure modes#

  • Cloud-save identity mismatch is a hard refusal. A slot, cloud policy, or control-scheme profile whose account differs from the header account is rejected at load (V2SaveGame.cpp:39,51,65) — two identities never merge by accident.
  • DSR without 2FA fails loud. buildV2ThemisPrivacyDsrWorkflow throws when authenticatedWith2Fa is false (v2-data-subject-rights.ts:172); a rectification with no fields throws too (:179). No silent no-op.
  • GDPR optional telemetry with no consent is dropped, not retained. The allowance evaluator rejects it with gdpr-explicit-opt-in-required (telemetry-privacy-compliance.ts:400); CCPA behavioral telemetry under a Do-Not-Sell opt-out is rejected with ccpa-do-not-sell-share (:387).
  • Anti-cheat can never auto-discipline. validateSuite (nous-anti-cheat-classifiers.ts:143) refuses any suite flagged as able to influence rollback, and automatedDisciplineAllowed is false by construction — classifier output only prioritises human review.
  • Aje/Web3 surfaces fail CI where banned. check-platform-cert-bans.py fails the build if a cosmetic-ownership or governance surface is reachable on a platform whose bannedSurfaces[] prohibits it (V2/legal/platform-cert-bans.json).
  • Missing region content data defaults to most-restrictive. Unknown regions fall through to the China profile rather than shipping uncut gore.

Where this connects#