# V1 Integration & Identity Bridge

V3 ("Lilith") is a metaverse, but it is not a separate product with its own
login, its own wallet, and its own copy of every privacy obligation. It is a new
embodiment surface bolted onto the **existing V1 Oshun platform**, and the
defining architectural choice — the same one V2 makes about compliance — is that
**V3 forks none of the platform's spine.** Identity, JWT issuance, OAuth client
registration, the audit trail, the event bus, and the contract codegen are all
shared `@oshun/*` packages that V1 already operates; V3 rides on them through a
thin set of route surfaces and one purpose-built translation library. A V3 user
is a V1 user — there is no V3-private account store — and the single seam that
turns a V1 account into an embodied, avatar-bearing presence in a room is
`libs/v3/lilith-identity-bridge` (`@oshun/lilith-identity-bridge`). That seam is
the subject of this page, alongside the BFF / contracts / auth / audit
integration points that surround it.

The grounding here is real and code-located. The bridge's binding, override,
reputation, per-region, and DSAR logic live in
`libs/v3/lilith-identity-bridge/src/{index,per-region-rules,recording-consent}.ts`;
the V3 contract surface lives in `libs/contracts/src/v3/`; the shared identity
foundation is `libs/shared/identity` (`@oshun/identity`) and
`libs/shared/auth-primitives` (`@oshun/auth-primitives`), with audit and bus in
`libs/shared/audit-platform` and `libs/shared/event-bus`. This page is the
platform-integration companion in the "Authoring, Data, and Platform
Integration" set; the section hub is
[../V3_ARCHITECTURE.md](../V3_ARCHITECTURE.md).

## What ships, honestly

The **identity bridge is real, substantive, server-authoritative code** — not a
descriptor shell. `libs/v3/lilith-identity-bridge/src/index.ts` (~1170 lines)
implements account-avatar binding, realm display-name overrides, a reputation
scoring model with graded bands, and an eight-category DSAR export, all as pure
discriminated-union functions backed by the `@oshun/lilith-identity-bridge`
automation suite (`src/__tests__/index.spec.ts`, `recording-consent.spec.ts`).
`per-region-rules.ts` ships five real jurisdiction profiles (US-CA, DE, GB, IN,
BR) with fan-token, age, content-rating, and residency evaluation;
`recording-consent.ts` ships the class and concert recording-consent decision
policy. The **V3 contracts are real Zod** under `libs/contracts/src/v3/` and
generate a real OpenAPI 3.1 document (`openapi.ts` →
`buildV3OpenApiDocument()`). The **shared identity, auth, audit, and event-bus
packages are real V1 packages**, each with their own test suites.

Three honest qualifications. **First**, the bridge is a _pure decision kernel_:
it has no database, opens no socket, and does not itself verify a JWT signature.
`bindV3AvatarToV1Account()` consumes an _already-verified_ external OAuth
account (carrying an `accessTokenHash` and `verifiedAtIso`) and _returns_ a
binding, a profile delta, an audit event, and an operator-console row — the
actual JWT verification happens upstream in `@oshun/auth-primitives` /
`@oshun/identity` `JwtService`, and the actual persistence + audit write happen
at the call sites (the world server and tenant services). The library decides;
the platform executes. **Second**, the capability descriptor's
`operationalMetric` strings (`account-avatar-link:8ms-budget`,
`reputation-band:16ms-budget`, `realm-claim:24ms-budget`) are descriptor labels,
not measured latencies, and `realm-claim` has no dedicated function — it is
covered by the per-region and bind paths. **Third**, the 24-hour avatar-swap
cooldown is a _contract field_ (`AvatarBindingSchema.swapCooldownUntil`) plus a
server-side enforcement obligation; the bridge stamps the binding but does not
run the cooldown clock. The sections below say which is which.

## V1 integration: BFF, contracts, auth, audit

V3 reaches into the V1 monorepo at four well-defined points. None of them
duplicate logic V1 already owns.

### BFF routes

V3 reuses V1's `apps/oshun/bff` rather than standing up a second gateway, and
adds a `/api/v3/lilith/*` route family for sessions, classes, concerts, artists,
venues, commerce, studio authoring, and the operator console. Two routes are
specific to the tiered client stack: `/api/v3/lilith/launch` is the tier-router
decision endpoint (it returns a `LilithLaunchDecision`), and
`/api/v3/pxstream/match` is the Pixel Streaming session matchmaker that proxies
to the `lilith-pxstream-relay` service. The critical isolation rule is that the
**world server and tenant services are never exposed directly to clients**: all
catalog, identity, billing, and scheduling calls go through the BFF, and all
real-time traffic goes through the Lilith Realtime Gateway. That keeps the
account root, the consent engine, and the audit pipeline on one trusted side of
the boundary, with the embodied client on the other.

### Contracts

V3-specific contracts live under `libs/contracts/src/v3/` and are plain Zod,
generated into OpenAPI by the _same_ V1 codegen path. `primitives.ts` defines
the shared axes — `V3TenantSchema` (`lilith-platform`, `lilith-commons`,
`tara-studio`, `saraswati-stage`), `V3RealmSchema`, `V3VisibilityBandSchema`,
`V3CitationRef`, and a `V3RoyaltySplit` that `superRefine`s to exactly 10000
basis points. `lilith.ts` carries the identity-bearing shapes: `AvatarBinding`
(`v1UserId`, `avatarId`, `avatarVersion`, `swapCooldownUntil`,
`provenanceBundleId`, `consentRecordId`), `LilithSession` (a `sessionToken`, the
`v1UserId`, the embedded `avatarBinding`, and a `gatewayPin`), and `Presence`
(which carries a `visibilityBand`). `consent.ts` defines `EmbodiedConsent` —
keyed on `v1UserId`, scoped per tenant/realm/feature (`camera`, `voice`,
`recording`, `voice-clone-use`, `motion-tracking`, `spatial-transcript`, …) with
an `evidenceRef` and a `revokedAt`. `openapi.ts` turns all of this into a single
`V3OpenApiDocument` via `toJSONSchema`, exposing `/api/v3/lilith/launch` plus
per-descriptor CRUD paths tagged by tenant. Contracts V3 _shares_ with V1 —
identity, billing, residency — are reused unchanged; see
[../../platform/contracts.html](../../platform/contracts.html).

### Auth

Auth roots in the platform, not in V3. The access path uses the V1 JWT
primitive: `@oshun/identity` defines `OshunTokenClaims` / `AccessTokenClaims`
(`sub`, `sid`, `role`, `permissions`) under a `DEFAULT_AUTH_CONFIG` of 15-minute
RS256 access tokens, and `@oshun/auth-primitives` provides the `JwtService`
(`createRsaJwtService` / `createHmacJwtService`), the `OAuthClientRegistry`, and
token-refresh / revocation. The bridge declares `@oshun/auth-primitives` as its
sole upstream package, which is the honest statement of where verification
lives. The world server validates the JWT on every connect; the Realtime Gateway
re-validates on the transport handshake; and UE `V3OnlineServices` bridges the
same JWT through the platform OAuth bridge for native console / headset sign-in.
This is the same identity foundation documented in
[../../platform/auth-identity.html](../../platform/auth-identity.html); V3
composes it. Notably, `@oshun/identity` already ships a `v2-account-binding`
module — the V3 bridge is the _third_ product to bind an embodied or
entitlement-bearing context onto the one account root, following an established
pattern rather than inventing one.

### Audit and the event bus

Every operator action, persona-policy decision, generation job, ticket
transaction, consent change, and royalty settlement is written to
`@oshun/audit-platform` with V3-scoped event types — the bridge emits exactly
these, e.g. `v3.account_avatar.linked`, `v3.realm_display_name.override`, and
`v3.session_dsar.export_packaged`. Because V3 reuses the V1 audit platform, it
inherits V1's audit-query, DSAR, and retention tooling instead of duplicating
it. Cross-shard continuity rides `@oshun/event-bus` (Redis Streams): presence,
takedown cascades, and continuity writes into V1 domains flow over the existing
bus. Two `libs/v3/*` adapters make the "compose, don't fork" thesis concrete —
`@oshun/memory-iris-spatial` writes scene/pose memories straight into the V1
Iris domain (`buildIrisMemoryWritePlan`, tagging pose memory as `biometric`
sensitive-category with consent records), and
`@oshun/sophia-saraswati-grounding` grounds artist backstories through the V1
`@oshun/evidence-sophia` evidence engine. Both are thin real adapters over V1
engines, not reimplementations.

## The identity and account bridge

`@oshun/lilith-identity-bridge` is the single translation layer between a V1
account and an embodied V3 session. It resolves who you are, which avatar you
wear, what name and reputation you carry into a realm, and what jurisdiction
rules apply — and it does so _server-authoritatively_, emitting typed decisions
that the world server persists.

```mermaid
flowchart LR
    v1["V1 Oshun account<br/><sub>@oshun/identity</sub>"]
    oauth["Headset OAuth issuers<br/>Quest · Vision Pro · PSVR 2"]
    jwt["JwtService verify<br/><sub>@oshun/auth-primitives</sub>"]
    bridge["<b>Lilith-Identity-Bridge</b><br/><sub>bindV3AvatarToV1Account()</sub>"]
    region["evaluateV3PerRegionRules()<br/><sub>US-CA · DE · GB · IN · BR</sub>"]
    out["Typed outputs"]
    profile["V1 profile delta<br/>v3.activeAvatarId"]
    audit["@oshun/audit-platform<br/>v3.account_avatar.linked"]
    world["World Server / BFF<br/>(persist + enforce)"]

    v1 --> jwt
    oauth --> jwt
    jwt --> bridge
    region --> bridge
    bridge --> out
    out --> profile
    out --> audit
    out --> world
    world --> profile
```

### Account resolution and the OAuth bridge

On session start a client presents a V1 JWT, which the platform verifies and
resolves to a `v1UserId` and a residency zone. Native console / headset sign-in
flows (Quest, Vision Pro, PSVR 2) arrive through the platform OAuth bridge:
`V3_SUPPORTED_OAUTH_BRIDGE_PLATFORMS` enumerates exactly those three, and
`v3OAuthBridgeIssuerForPlatform()` pins each to its canonical issuer
(`https://auth.meta.com/quest`, `https://appleid.apple.com/vision-pro`,
`https://ca.account.sony.com/psvr2`). `bindV3AvatarToV1Account()` is the entry
point: it normalizes the external account, derives an `externalAccountKey` of
the form `${platform}:${externalAccountId}`, and **rejects any account whose
issuer does not match the bridge platform** (`oauth-account-not-verified`) — the
issuer-binding test
(`'rejects an OAuth account whose issuer does not match the bridge platform'`)
guards exactly this. A successful bind upserts the linked external account and
the avatar binding into the profile, returns the new `externalAccountKey`, and
emits a `v3.account_avatar.linked` audit event. The same account can link Quest
_and_ Vision Pro _and_ PSVR 2 against one V1 profile without losing the active
avatar — the multi-platform test asserts that all three collapse to one account
shape.

### Avatar binding and the active avatar

There is exactly one active avatar per session. `bindV3AvatarToV1Account()`
stamps the binding with `profileField: 'v3.activeAvatarId'` and
`persistedToV1Profile: true`, and `v3AvatarIdVisibleInV1Profile()` confirms
read-back consistency: an avatar is "visible" only when it is the profile's
`activeAvatarId` _and_ appears as a persisted binding. The active avatar id
lives on the immutable V1 profile, not a V3-private store — which is what lets a
DSAR or a deletion reach it through the V1 pipeline. The anti-harassment 24-hour
swap cooldown is represented in the contract (`AvatarBinding.swapCooldownUntil`)
and enforced server-side; the bridge library produces the binding but, by
design, does not itself run the cooldown timer (see "What ships, honestly"). The
richer avatar mechanics — MetaHuman/VRM dual-authoring, costume rules,
provenance bundles — belong to the avatar pipeline and the persona/rights layer,
not to the bridge.

### Display name, reputation, and presence privacy

Three identity-adjacent decisions round out the bridge. **Display name**
defaults to the V1 Oshun display name, but a per-realm override is permitted as
an anti-stalking measure: `applyV3RealmDisplayNameOverride()` sanitizes the
input (collapses whitespace, strips control characters, caps at 32 characters),
writes a `v3.realm_display_name.override` entry with a synthesized
`v1AuditLogId`, and emits a `V3OperatorDisplayNameOverrideRow` for the Lilith
Operator Console; overriding the same realm again replaces the prior alias while
other realms' aliases survive (both behaviours are tested). **Reputation** is
never exposed as a numeric score to users: `deriveV3ReputationSummary()`
computes a real weighted score from instructor ratings (`(avg − 3) × 12` quality
plus a `log2` volume term), audience sentiment
(`positiveRatio × 24 − negativeRatio × 20` plus volume), completed safety
reviews, and penalties (−18 per confirmed policy violation, −28 per confirmed
harassment report), then collapses it into graded bands — `new` (< 45), `known`
(≥ 45), `trusted` (≥ 75), and a hard `flagged` band whenever a confirmed
harassment report exists, which also sets `operatorEscalationRequired`.
`buildV3OperatorReputationRows()` sorts the moderation queue flagged-first.
**Presence privacy** carries the user's "online in Lilith" visibility (the
contract's `Presence.visibilityBand`, V1 default plus per-realm override) into
the digest the world server broadcasts.

### Per-region claims at bind time

Jurisdiction is decided at the bridge, not improvised per feature.
`evaluateV3PerRegionRules()` takes a profile id, tenant, age, requested
content-rating, requested fan-token benefit, and storage/compute zones, and
returns an `allow` / `adapt` / `block` decision. The five wave-1 profiles encode
real policy: the US-CA profile permits a `native-token` fan economy and pins
storage to `us-west`; DE/GB/IN/BR fall back to a `v1-account-benefit` (an
_adaptation_, not a block) and pin to `eu-de` / `uk` / `in` / `br` respectively;
age floors take the max of the account minimum and the per-tenant floor
(Saraswati Stage's 16+ over a 13+ account); content above the regional rating is
blocked (India caps at `teen`). `buildV3PerRegionRulesJurisdictionReport()` runs
five fixtures and asserts all five profiles are covered with every dimension
applied — the test confirms zero errors, and the block-path test shows underage
Saraswati access, over-rating content, and residency drift each producing a
`block` with the precise `blockedReasons`. The residency _legality_ of moving
data across these zones is decided upstream by `@oshun/data-residency`; the
bridge decides which profile applies and emits the audit tags.

### DSAR continuity

A V3 user's data is part of the V1 subject record. `buildV3SessionDsarExport()`
assembles eight categories — avatar history, voice transcripts, attendance,
signed-edition holdings, recordings, Pixel Streaming sessions, consent
decisions, and the ticket/refund/credit ledger — into a manifest, a V1 review
action (`generate_export_package`), and a `v3.session_dsar.export_packaged`
audit event, and **fails loud** if any category is missing
(`missing-v3-session-data`) or an encrypted package lacks a key reference
(`missing-encryption-key`). `buildV3SessionDsarV1PipelineCoverageReport()`
derives a `fullCoverage` flag from the manifest, proving the export flowed into
the V1 `admin-dsar-review` pipeline with every category present. Deletion
cascades the same way — across Postgres rows, S3 media (recording re-render with
the user redacted), and Redis hot state — because the identity binds through one
root.

## Edge cases and failure modes

- **Issuer spoofing is rejected.** A headset account whose `issuer` does not
  equal the canonical platform issuer returns `oauth-account-not-verified`; the
  bridge will not bind an avatar to an unverifiable external account.
- **The bridge decides, it never persists.** Every public function returns a
  typed result (binding, profile delta, audit event, operator row); if the world
  server never writes it, nothing happened. There is no hidden side effect to
  audit around.
- **Reputation degrades to a band, never a number.** A confirmed harassment
  report forces the `flagged` band and operator escalation regardless of an
  otherwise high score; the numeric score is internal and never surfaced to
  users.
- **Display names can't smuggle control characters.** The override sanitizer
  strips control codes and caps length before anything reaches the audit log or
  the operator console.
- **Region adapts before it blocks.** A fan-token request in a non-native-token
  market becomes a `v1-account-benefit` adaptation, not a hard failure; only
  age, over-rating, and residency drift produce a `block`.
- **DSAR refuses partial coverage.** An export missing any of the eight
  categories is an error, not a smaller bundle — coverage is a gate, not a
  best-effort.

## How it connects

The bridge sits between the platform spine and the embodied runtime. The
[world server and gateway](./world-server-and-gateway.md) validates the V1 JWT
on connect, persists the `v3.activeAvatarId` profile delta and the
checkpoint/presence state the bridge stamps, and enforces the swap cooldown the
bridge only records.
[Data architecture, tenancy, and residency](./data-tenancy-and-residency.md)
consumes the residency zone and tenant context the bridge resolves, routing
every durable row through `@oshun/data-residency` — the two pages are the
identity half and the storage half of the same compliance story. The persona,
tone, provenance, and rights decisions that act on a _bound_ identity are owned
by
[persona policy, provenance, and rights](./persona-policy-provenance-and-rights.md),
which the bridge feeds the resolved account, reputation band, and consent
context. Upstream, everything composes the platform foundations — the JWT and
OAuth primitives in [Auth & Identity](../../platform/auth-identity.html), the
shared schema codegen in [Contracts](../../platform/contracts.html), and the
`@oshun/*` domain libraries catalogued in
[Oshun Domain Libraries](../../platform/oshun-domain-libraries.html).

## Related

- [World Server and Gateway](./world-server-and-gateway.md) — validates the JWT,
  persists the binding, and enforces the cooldown the bridge stamps
- [Data Architecture, Tenancy & Residency](./data-tenancy-and-residency.md) —
  the storage/residency half that reads the bridge's zone and tenant context
- [Persona Policy, Provenance, and Rights](./persona-policy-provenance-and-rights.md)
  — the policy layer that acts on a bound identity
- [Auth & Identity](../../platform/auth-identity.html),
  [Contracts](../../platform/contracts.html) — the platform primitives V3
  composes (`@oshun/identity`, `@oshun/auth-primitives`, the Zod→OpenAPI
  codegen)
- [Oshun Domain Libraries](../../platform/oshun-domain-libraries.html) — where
  `@oshun/lilith-identity-bridge`, `@oshun/memory-iris-spatial`, and
  `@oshun/sophia-saraswati-grounding` sit in the domain map
- The section hub: [../V3_ARCHITECTURE.md](../V3_ARCHITECTURE.md)
