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.
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 superRefines 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.
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; 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.
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
issuerdoes not equal the canonical platform issuer returnsoauth-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
flaggedband 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-benefitadaptation, not a hard failure; only age, over-rating, and residency drift produce ablock. - 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 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
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,
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, the
shared schema codegen in Contracts, and the
@oshun/* domain libraries catalogued in
Oshun Domain Libraries.
Related#
- World Server and Gateway — validates the JWT, persists the binding, and enforces the cooldown the bridge stamps
- Data Architecture, Tenancy & Residency — the storage/residency half that reads the bridge's zone and tenant context
- Persona Policy, Provenance, and Rights — the policy layer that acts on a bound identity
- Auth & Identity,
Contracts — the platform primitives V3
composes (
@oshun/identity,@oshun/auth-primitives, the Zod→OpenAPI codegen) - Oshun Domain Libraries — where
@oshun/lilith-identity-bridge,@oshun/memory-iris-spatial, and@oshun/sophia-saraswati-groundingsit in the domain map - The section hub: ../V3_ARCHITECTURE.md