Oshun Platform · Architecture

Security, Privacy, and Compliance

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

11sections12 minread3tables

On this page

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 layer, underneath the High-Level Architecture and beside Trust, Safety, and Privacy. Most of what follows is real code with tests already in the repo.

Product scope: V1/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 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 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.


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 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.
  • 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.