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:
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:
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":
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 therequiredScope, then enforces tenant isolation: a cross-tenant target (subjectTenantId !== targetTenantId) is denied withreason: 'tenant-isolation'unless the subject isadmin-leadershiporprivacy-operator(the two roles allowed to cross tenants for global ops).authorizeObjectAccess()— object-level (BOLA) authorization. The owner and explicitly granted users pass;admin-leadershipalways passes; and read-only support/moderator/privacy operators may read but not mutate. Everything else returnsreason: '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.tsvalidates a bundle againstProvenanceBundleSchema.parse, runsverifyProvenanceBundle, and refuses to attach when verification fails — a fail-loud seam, not a silent pass. It also drivesprovenance-badges.tsandprovenance-bundle-validation.synthetic-media-labeling.tsis a realSyntheticMediaLabelingServicewithLabelVerdictandLabelRenderCheckmachinery that verifies the "synthetic content" indicator is actually rendered on public profiles and shared artifacts — the label is checked, not assumed.watermark-verification.tsverifies watermark hashes on shared/exported media.voice-likeness-consent.tsmodels consent for voice and likeness cloning: aConsentModalityofvoice | likeness_image | likeness_video | full_avatar, aConsentCommercialScope, verification methods/status, and a hardCONSENT_NON_NEGOTIABLE_BLOCKSlist —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.tsdefinesTombstonePolicyand a frozenV1_TOMBSTONE_POLICIESregistry, withgetTombstonePolicy(schemaName)and atombstone-semantics.test.tsenforcing 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.tsmodels aDsarDeletionRequestfanned acrossDsarContractEraserandDsarMemoryErasertargets, producing aDsarDeletionReceiptwith per-targetDsarTargetOutcomerecords — so an erasure is provably complete (or provably partial) rather than fire-and-forget.dsar-erasure-runtime.tsruns it, with both unit and.integration.test.tscoverage.
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:
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.tsactually usesioredisRedispub/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 NXclaim keys to emulate consumer groups. There is noXADD/XREAD/XREADGROUP/XGROUP— it is explicitly not native Streams. See Communication Patterns. - Common contracts count.
libs/contracts/src/common/holds 285 files (155 implementation.tsmodules plus 6.d.tsdeclarations and their specs), not the stale "≈265." - Connector count.
libs/shared/inbound-integrations/src/index.tsexports 15 connector modules, not the 9 the older table lists — it also includeslti-verification.ts, two SCORM runtimes (scorm-rte.ts,scorm-2004-rte.ts),byom-model.ts(separate frombyom.ts), andcalendar-google-transport.ts.
Related#
- Foundations — the shared substrate this page secures
- Communication Patterns — the BFF write lifecycle and event-bus the security middleware fronts
- Data Architecture and Tenancy — residency, home zones, and per-domain databases
- Trust, Safety, and Privacy — product-side consent, moderation, and safety framing
- Observability, Design System, Testing, and Performance — audit, eval, and release-gate companion
- ../ARCHITECTURE.md — the architecture hub