# Security, Compliance & Sister-Monorepo Integration

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](../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](./online-backbone-and-competitive-integrity.md));
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](../../platform/auth-identity.html) 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.

## Privacy: PII stripping, consent & content gating

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.

```mermaid
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**:

```mermaid
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

- **Sideways:**
  [Online Backbone & Competitive Integrity](./online-backbone-and-competitive-integrity.md)
  (the anti-cheat + ban path and the gRPC/proto honesty),
  [Telemetry, Performance, Testing & Release Gates](./telemetry-performance-testing-and-release-gates.md)
  (PII strip at ingest, cert as a release gate), and
  [Live-Ops, Store, Progression & Community](./live-ops-store-progression-and-community.md)
  (store compliance, spend limits, community governance).
- **Platform foundations it composes:**
  [Auth & Identity](../../platform/auth-identity.html),
  [Persistence & Data](../../platform/persistence-data.html), and the
  [shared libraries](../../platform/shared-libraries.html)
  (`@oshun/data-residency`, `@oshun/audit-platform`, `@oshun/region-rules`).
- The section hub: [../V2_ARCHITECTURE.md](../V2_ARCHITECTURE.md).
