# Security, Privacy, and Compliance

This page is the consolidated security, privacy, and compliance surface for
Oshun V1: how identity is established, how authorization is scoped, how
sensitive actions are gated, where secrets live, how every artifact and mutation
is made provenance-bearing and tamper-evident, and how a user's right to
consent, export, and erasure is enforced down to the row. It serves engineers
building any customer domain (no domain re-implements auth, residency, or audit)
and operators running DSAR, moderation, and break-glass workflows. It sits in
the [Foundations](./foundations.md) layer, underneath the
[High-Level Architecture](./high-level-architecture.md) and beside
[Trust, Safety, and Privacy](./trust-safety-and-privacy.md). Most of what
follows is real code with tests already in the repo.

Product scope:
[`V1/features.md` § Security, Access Control, and Operational Hardening](../features.md#security-access-control-and-operational-hardening).
Backlog: §28.

> **Status candor.** This area is overwhelmingly _implemented_, not
> aspirational. The role/scope model, the OAuth 2.1/PKCE state machine, step-up
> evaluation, the secrets-rotation and SSRF/egress controls, the data-residency
> enforcer, the append-only hash-chained audit substrate, the DSAR
> deletion-cascade machinery, tombstone semantics, and the
> synthetic-media/provenance/voice-likeness-consent modules all exist as real
> code with tests. Two honest caveats carry through the whole page: the OAuth
> module is **contracts/state-machine level** — token types, PKCE validation
> patterns, refresh rotation and revocation cascade — and does **not** stand up
> a live authorization-server deployment; and secrets in production resolve from
> AWS Secrets Manager backed by KMS, an infrastructure binding outside this
> repo. Where something is provider-gated or deployment-level, this page says
> so.

---

## Where the building blocks live

Security in V1 is not one library; it is a small set of composable substrates
that the [BFF request lifecycle](./communication-patterns.md) and every domain
adapter draw on. The five homes worth memorizing:

| Concern                      | Package / path                                                     | What it owns                                                                                                        |
| ---------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| Platform security primitives | `@oshun/platform-foundations` (`libs/oshun/platform-foundations/`) | Roles/scopes, OAuth 2.1/PKCE, step-up, secrets, rollback, abuse controls                                            |
| Low-level auth primitives    | `@oshun/auth-primitives` (`libs/shared/auth-primitives/`)          | JWT, sessions, API keys, TOTP, OAuth client/revoke, token refresh, password, tenant isolation                       |
| Workspace identity client    | `@oshun/auth-client` (`libs/oshun/auth/`)                          | Identity/session client used by every Oshun app                                                                     |
| Audit & provenance           | `@oshun/audit-platform` (`libs/shared/audit-platform/`)            | Hash-chained audit log, retention, escalation, synthetic-media labeling, voice/likeness consent, provenance bundles |
| Residency & DSAR             | `libs/shared/data-residency/`, `libs/oshun/persistence/`           | Home-zone routing, residency enforcement, deletion cascade, tombstones                                              |

`@oshun/platform-foundations` is the single package that re-exports the security
core. Its `libs/oshun/platform-foundations/src/index.ts` surfaces exactly nine
subsystems, each a barrel of its own:

```ts
export * from './service-discovery/index';
export * from './public-api/index'; // OAuth 2.1 / PKCE
export * from './shared-contracts/index';
export * from './role-model/index'; // CANONICAL_ROLES, scopesFor, authorizeAction
export * from './step-up/index'; // SENSITIVE_ACTIONS, evaluateStepUp
export * from './secrets/index'; // rotation, scoped access
export * from './configs/index';
export * from './rollback/index';
export * from './abuse-controls/index'; // egress / SSRF / per-key abuse
```

---

## Authentication and the public API surface

Every Oshun app uses `@oshun/auth-client` for workspace identity and session;
tokens carry the scopes declared in the domain registry's auth policy. Beneath
that client, `@oshun/auth-primitives` is far richer than "JWT and session
primitives" — `libs/shared/auth-primitives/src/` ships `jwt.ts`, `session.ts`,
`api-key.ts`, `password.ts`, `totp.ts` (TOTP for step-up), `oauth-client.ts`,
`oauth-revoke.ts`, `token-refresh.ts`, `token-audit.ts`, `platform-roles.ts`,
and `tenant-isolation.ts`. These are the primitives the higher-level
platform-foundations OAuth machine composes.

### OAuth 2.1 with mandatory PKCE

The public-API authorization surface lives at
`libs/oshun/platform-foundations/src/public-api/oauth.ts` (the "§27 Public API
platform" module). It is a deterministic, side-effect-free state machine — it
_validates_ and _transitions_ token state but does not run an HTTP authorization
server. That is the honest scope caveat noted above. It models three grant types
and two token types:

```ts
export const OAUTH_GRANT_TYPES = [
  'authorization-code',
  'refresh-token',
  'client-credentials',
] as const;
export const TOKEN_TYPES = ['access', 'refresh'] as const;
```

PKCE is first-class and S256-only. Two regexes enforce the RFC 7636 shapes: a
code challenge of `/^[A-Za-z0-9_-]{43,128}$/` and a verifier of
`/^[A-Za-z0-9._~-]{43,128}$/`. `verifyPkceChallenge()` recomputes the challenge
by running `base64UrlSha256(verifier)` (over `@noble/hashes` `sha256`) and
comparing it to the stored challenge with a constant-time
`timingSafeEqualString()` to avoid leaking match position.
`validateAuthorizationCodeRequest()` emits a typed error list including
`'pkce-required'`, `'invalid-code-challenge'`, `'unsupported-challenge-method'`,
`'invalid-redirect-uri'`, `'scope-not-allowed'`, and `'client-mismatch'`;
`validateAuthorizationCodeRedeem()` adds `'code-expired'`, `'code-consumed'`
(single-use enforcement), and `'pkce-verification-failed'`.

Refresh handling is rotation-based, not reuse-based. `redeemRefreshToken()`
mints a _new_ access token and a _new_ refresh token in one step and marks the
prior refresh token `revokedAtUnixSeconds`; `revocationCascade()` then revokes
any access token whose `mintingRefreshTokenId` points at a revoked refresh
token, so revoking the refresh root invalidates everything it minted.
`validateAccessToken()` returns granular failures — `'wrong-token-type'`,
`'revoked'`, `'expired'`, `'scope-missing'`, `'tenant-mismatch'`,
`'rate-limit-exceeded'` — and `evaluatePublicApiQuota()` enforces per-key
windowed quotas with `'quota-exhausted'`/`'invalid-cost'`/`'invalid-window'`
verdicts. Every decision can be journaled through `buildPublicApiAuditEvent()`,
whose `eventType` enumerates `oauth.authorize.requested`, `oauth.code.redeemed`,
`oauth.token.refreshed`, `oauth.token.revoked`, `public_api.quota.checked`,
`public_api.request.authorized`, and `public_api.request.denied`.

---

## Authorization: the canonical role/scope model

`libs/oshun/platform-foundations/src/role-model/role-model.ts` defines the
canonical role set. It is **ten** roles — broader than the prose elsewhere in
the docs, which describes about eight and uses looser names like "support" or
"admin leadership":

```ts
export const CANONICAL_ROLES = [
  'customer',
  'creator',
  'support-agent',
  'reviewer',
  'moderator',
  'privacy-operator',
  'model-operator',
  'persona-operator',
  'tenant-admin',
  'admin-leadership',
] as const;
```

Two roles the older prose never surfaced as distinct are worth calling out:
`creator` (a customer who also holds `creator.studio`) and `tenant-admin` (the
per-tenant console operator). Note also that the canonical name is
`support-agent`, not the looser "support".

Scopes are the unit of authorization, not roles. `SCOPE_KEYS` enumerates 21
least-privilege scopes (`customer.self`, `creator.studio`, `support.case.read`,
`review.queue.decide`, `moderation.decide`, `privacy.dsar.execute`,
`model.registry.promote`, `persona.registry.publish`, `tenant.console`,
`admin.breakglass.act`, `admin.audit.global`, …), and `scopesFor(role)` maps
each role to the exact set it carries:

| Role               | Representative scopes                                                                |
| ------------------ | ------------------------------------------------------------------------------------ |
| `customer`         | `customer.self`, `customer.shell`                                                    |
| `creator`          | + `creator.studio`                                                                   |
| `support-agent`    | `support.case.read`, `support.case.write`                                            |
| `reviewer`         | `review.queue.read`, `review.queue.decide`                                           |
| `moderator`        | + `moderation.decide`                                                                |
| `privacy-operator` | `privacy.dsar.execute`, `privacy.audit.read`, `admin.audit.global`                   |
| `model-operator`   | `model.registry.read`, `model.registry.promote`                                      |
| `persona-operator` | `persona.registry.read`, `persona.registry.publish`                                  |
| `tenant-admin`     | `tenant.console`, `admin.audit.global`                                               |
| `admin-leadership` | `admin.users.act`, `admin.scope.grant`, `admin.breakglass.act`, `admin.audit.global` |

Three enforcement functions defend against three distinct OWASP-API-style
failure modes:

- **`authorizeAction()`** — function-level authorization. It checks that the
  subject holds the `requiredScope`, then enforces tenant isolation: a
  cross-tenant target (`subjectTenantId !== targetTenantId`) is denied with
  `reason: 'tenant-isolation'` unless the subject is `admin-leadership` or
  `privacy-operator` (the two roles allowed to cross tenants for global ops).
- **`authorizeObjectAccess()`** — object-level (BOLA) authorization. The owner
  and explicitly granted users pass; `admin-leadership` always passes; and
  read-only support/moderator/privacy operators may read but not mutate.
  Everything else returns `reason: 'object-not-owned'`.
- **`filterFieldsByRole()`** — excessive-data-exposure protection. It projects a
  response down to the union of fields allowed for the subject's roles, so a
  contract can return a wide object internally and the role filter narrows what
  any given operator actually sees.

---

## Step-up authentication for sensitive actions

High-blast-radius operations require a recent, factor-backed re-assertion of
identity, evaluated by `libs/oshun/platform-foundations/src/step-up/step-up.ts`.
`SENSITIVE_ACTIONS` is the canonical gated list — twelve actions, each with a
default freshness budget:

| Action                                                                                                          | Default step-up freshness |
| --------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `issue-legal-hold`                                                                                              | 2 min                     |
| `delete-account`, `export-personal-data`, `transfer-data-cross-region`, `override-safety-gate`, `promote-model` | 5 min                     |
| `rotate-api-key`, `remove-cloned-voice`, `remove-cloned-avatar`, `publish-persona`                              | 10 min                    |
| `change-billing-method`, `configure-tenant-policy`                                                              | 15 min                    |

`evaluateStepUp()` returns `{ verdict: 'allow' }` only when the last step-up is
within budget and any `requiredFactors` (`mfa-totp`, `mfa-webauthn`,
`biometric`, `reauth-password`) were satisfied during that step-up; a
future-dated assertion is rejected. Otherwise it returns `step-up-required` with
a reason of `'stale'`, `'missing-factor'`, or `'future-assertion'`. The
domain-level wiring of these actions (e.g. assistant memory export, account
deletion, admin inspection) is declared per domain alongside the registry's auth
policy.

---

## Secrets, abuse controls, and egress safety

**Secrets.** In production, secrets resolve from AWS Secrets Manager backed by
KMS; in development, `.env` files (never committed) carry the same keys, and
RunPod endpoint IDs live in Secrets Manager. The
`libs/oshun/platform-foundations/src/secrets/secrets.ts` module gives this a
typed shape: a `SecretPurpose` of `provider-api-key`, `tenant-encryption-key`,
`voice-provider-credential`, `webhook-signing-key`, or `oauth-client-secret`;
and a `SecretScope` of `global | tenant | service`. It also carries rotation
logic (`evaluateRotation()`, `rotateSecret()`) plus scoped access checks
(`authorizeSecretAccess()`), so a tenant-scoped secret cannot be read across
tenant boundaries.

**Abuse and egress.** `libs/oshun/platform-foundations/src/abuse-controls/` adds
`enforceAbuseControls()` and `checkEgress()`, the latter producing typed
`EGRESS_DENIAL_REASONS` so outbound calls to user-controlled URLs are policed.
At the HTTP layer, `@oshun/http-client` (`libs/shared/http-client/`) carries the
matching defensive middleware: `ssrf-guard.ts` classifies IPv4/IPv6 addresses
(`classifyIp`, `isUnsafeIpClass`, `assertUrlAllowed`, `makeSsrfSafeLookup`) to
block requests to private/loopback/link-local ranges, and `circuit-breaker.ts`,
`retry.ts`, `tenant-context.ts`, and `tracing.ts` round out the resilient-client
stack. The same library owns the idempotency middleware (`idempotency.ts`:
`DEFAULT_IDEMPOTENCY_HEADER = 'Idempotency-Key'`,
`DEFAULT_IDEMPOTENCY_TTL_MS = 24h`, `IdempotencyMiddleware`,
`createIdempotencyMiddleware()`) that fronts the
[BFF write lifecycle](./communication-patterns.md) so replays return the cached
envelope instead of re-executing a sensitive write.

---

## Provenance and synthetic-content integrity

Every generated artifact carries a `ProvenanceBundle` — consent ID, prompt,
model, watermark hash, timestamp, invoking user, and tenant — and shared or
exported Living Scenes carry C2PA tags. This is not a single sentence's worth of
code: `@oshun/audit-platform` (`libs/shared/audit-platform/src/`) is a
~40-implementation-module substrate, and several modules are dedicated to
content integrity:

- **`provenance-attachment.ts`** validates a bundle against
  `ProvenanceBundleSchema.parse`, runs `verifyProvenanceBundle`, and _refuses to
  attach_ when verification fails — a fail-loud seam, not a silent pass. It also
  drives `provenance-badges.ts` and `provenance-bundle-validation`.
- **`synthetic-media-labeling.ts`** is a real `SyntheticMediaLabelingService`
  with `LabelVerdict` and `LabelRenderCheck` machinery that verifies the
  "synthetic content" indicator is actually rendered on public profiles and
  shared artifacts — the label is checked, not assumed.
- **`watermark-verification.ts`** verifies watermark hashes on shared/exported
  media.
- **`voice-likeness-consent.ts`** models consent for voice and likeness cloning:
  a `ConsentModality` of
  `voice | likeness_image | likeness_video | full_avatar`, a
  `ConsentCommercialScope`, verification methods/status, and a hard
  `CONSENT_NON_NEGOTIABLE_BLOCKS` list — `csam`, `ncii`, `political_deception`,
  `defamation`, `impersonation_of_other_real_person`, `harassment`,
  `medical_diagnosis_impersonation`, `legal_advice_impersonation` — that can
  never be consented around.

---

## The append-only audit substrate

The audit trail is tamper-evident, not merely append-only. `hash-chain.ts`
implements a hash-chained event log: `hashEvent()` produces a stable hash of a
`CanonicalPlatformAuditEvent` (via `canonicalSerialize()` for order-independent
serialization), `chainHashStep(previousChainHash, eventHash)` links each entry
to its predecessor, and verification (`AuditChainVerificationResult`,
`AuditChainMismatch`) raises `AuditChainTamperDetectedError` the moment any
historical entry is altered. This is because altering a historical entry changes
every downstream chain hash. The `HashChainedAuditEventStore` (and its in-memory
test double `InMemoryAuditChainLog`) is the substrate behind Iris admin
inspection, DSAR review, the deletion workflow, and review packages.

The surrounding modules make it operable: `retention.ts` (retention windows),
`schema-versioning.ts` (audit-event schema evolution), `escalation-rules.ts` and
`support-escalation.ts` (operator escalation), `compliance-attestation.ts`
(attestation records), and integration specs such as
`escalation-override-completeness.integration.spec.ts` and
`compliance-export.integration.spec.ts`. The data-residency layer bridges into
the audit log via an injected `ResidencyAuditPublisher` seam (exercised by
`audit-platform-bridge.spec.ts`), so a residency decision is itself an auditable
event.

---

## Privacy: consent, export, and erasure

Iris owns consent, deletion, and export, and DSAR (Data Subject Access Request)
review runs through the admin web. The right-to-erasure machinery is real and
lives in `libs/oshun/persistence/src/`:

- **Tombstone semantics on every user-data table.** `tombstone-semantics.ts`
  defines `TombstonePolicy` and a frozen `V1_TOMBSTONE_POLICIES` registry, with
  `getTombstonePolicy(schemaName)` and a `tombstone-semantics.test.ts` enforcing
  that every user-data contract declares one. Writes are tombstone-aware so a
  deleted row leaves an auditable marker, not a silent gap.
- **DSAR deletion cascade.** `dsar-deletion-cascade.ts` models a
  `DsarDeletionRequest` fanned across `DsarContractEraser` and
  `DsarMemoryEraser` targets, producing a `DsarDeletionReceipt` with per-target
  `DsarTargetOutcome` records — so an erasure is provably complete (or provably
  partial) rather than fire-and-forget. `dsar-erasure-runtime.ts` runs it, with
  both unit and `.integration.test.ts` coverage.

Per-domain redaction policies apply at capture time (camera, microphone, photo
library), and synthetic-content indicators are preserved on public profiles and
shared artifacts (enforced by the synthetic-media-labeling service above).

### Partial-failure honesty

DSAR and other multi-target operations report partial success through a
first-class Zod contract rather than an ad-hoc shape, at
`libs/contracts/src/common/partial-failure-envelope.ts`. The envelope is
`{ results: unknown[], errors: PartialFailureError[], partial: boolean }`, where
`PartialFailureError = { domain, stage?, message }`, and a `superRefine`
cross-field rule makes the flag honest — `partial: true` requires at least one
error, `partial: false` requires an empty `errors` array:

```ts
export const PartialFailureEnvelopeSchema = z
  .object({
    results: z.array(z.unknown()),
    errors: z.array(PartialFailureErrorSchema),
    partial: z.boolean(),
  })
  .superRefine(/* partial ⇔ errors non-empty */);
```

`createPartialFailureEnvelopeSchema(resultSchema, errorSchema?)` is the generic
factory for typed result/error pairs, and `buildPartialFailureEnvelope()`
constructs one while _throwing_ if the supplied `partial` flag contradicts the
errors present — you cannot claim a clean run with errors in hand.

---

## Data residency and tenant isolation

The residency enforcer is real, at `libs/shared/data-residency/src/enforcer.ts`:
`ResidencyEnforcementService` (constructed via
`createResidencyEnforcementService()`) takes a `ResidencyEnforcementRequest`
against a `ResidencyEnforcementContext` and returns a
`ResidencyEnforcementOutcome` — allow, or require consent. It also emits a
`ResidencyAuditEvent` through a `ResidencyAuditPublisher` and raises
`ResidencyEnforcementError` on a hard violation. The surrounding modules cover
the full path: `home-zone.ts` resolves a tenant's home zone,
`traffic-shaping.ts` shapes cross-region traffic, `dsr-routing.ts` routes data
subject requests to the right region, and an injected `ResidencyAuditPublisher`
seam (covered by `audit-platform-bridge.spec.ts`) records every decision into
the hash-chained log.

Tenant isolation is enforced at three layers and _tested_ for leakage:
`authorizeAction()`'s `'tenant-isolation'` denial at the authorization layer,
`@oshun/auth-primitives`' `tenant-isolation.ts` at the primitive layer, and
cross-tenant leakage tests under `tests/security/tenant-isolation/`. Because
tenant context flows through the [BFF lifecycle](./communication-patterns.md)
before the body is even validated, a cross-tenant request short-circuits before
it can touch owned data.

---

## Accuracy notes for readers of older docs

A few claims elsewhere in V1 are stale or imprecise; this page is the corrected
reference:

- **The event bus is not Redis Streams.** Older prose says the bus runs "over
  Redis Streams." `libs/shared/event-bus/src/event-bus.ts` actually uses
  `ioredis` `Redis` pub/sub for fan-out, a TTL-bounded key (`DEFAULT_EVENT_TTL`
  = 86400s) as the replay source, a sorted set (`scheduled`) for delay/nack, a
  list+hash pair for the dead letter, and per-`(eventId, group)` `SET NX` claim
  keys to emulate consumer groups. There is **no**
  `XADD`/`XREAD`/`XREADGROUP`/`XGROUP` — it is explicitly _not_ native Streams.
  See [Communication Patterns](./communication-patterns.md).
- **Common contracts count.** `libs/contracts/src/common/` holds **285** files
  (155 implementation `.ts` modules plus 6 `.d.ts` declarations and their
  specs), not the stale "≈265."
- **Connector count.** `libs/shared/inbound-integrations/src/index.ts` exports
  **15** connector modules, not the 9 the older table lists — it also includes
  `lti-verification.ts`, two SCORM runtimes (`scorm-rte.ts`,
  `scorm-2004-rte.ts`), `byom-model.ts` (separate from `byom.ts`), and
  `calendar-google-transport.ts`.

---

## Related

- [Foundations](./foundations.md) — the shared substrate this page secures
- [Communication Patterns](./communication-patterns.md) — the BFF write
  lifecycle and event-bus the security middleware fronts
- [Data Architecture and Tenancy](./data-architecture-tenancy.md) — residency,
  home zones, and per-domain databases
- [Trust, Safety, and Privacy](./trust-safety-and-privacy.md) — product-side
  consent, moderation, and safety framing
- [Observability, Design System, Testing, and Performance](./observability-and-quality.md)
  — audit, eval, and release-gate companion
- [../ARCHITECTURE.md](../ARCHITECTURE.md) — the architecture hub
