# Architecture, Platform Foundations, and Security

This page describes the load-bearing layer beneath every Oshun V1 domain: the
canonical contracts, the persistence-alignment harness, the event and queue
substrate, the inbound/outbound integration plumbing, the public-API
authorization model, and the security controls that gate launch. It serves
platform engineers, integrators, operators, and security reviewers — anyone who
needs to know how a request travels from the shell to a domain service and back,
what guarantees hold along the way, and which pieces are real versus spec-only.
It sits "below" the customer-facing domain pages ([Tara](./domain-tara.md),
[Arete](./domain-arete.md), [Veritas](./domain-veritas.md),
[Nyx](./domain-nyx.md), [Nisaba](./domain-nisaba.md),
[Metis](./domain-metis.md)) and underneath the channel surfaces
([Channel Abstraction](./channel-abstraction-and-boundary.md)); nearly all of it
is implemented code with tests, not aspiration. Where a piece is contract-level
or provider-gated, this page says so explicitly.

> Candor note up front. This layer is overwhelmingly **implemented**: the
> contracts, the persistence drift/index/migration/tombstone test quartet, the
> OAuth 2.1/PKCE state machine, the role/scope model, the partial-failure
> envelope contract, the durable queue + dead-letter + SLA monitor, the
> topic-registry + outbound delivery + webhook-simulator event bus, the inbound
> connectors, the data-residency enforcer, the idempotency middleware, and the
> append-only audit hash chain all exist as real code. Two things are honestly
> narrower than they sound: the OAuth module is a **contracts/state-machine**
> implementation (token types, PKCE validation, refresh, revocation cascade) —
> not a deployed live authorization server — and the event bus is **not** built
> on native Redis Streams (see the correction below). Both are called out where
> they appear.

## Where the foundations live

The platform-foundations package is the single named home for the cross-cutting
subsystems. `libs/oshun/platform-foundations/src/index.ts` re-exports exactly
**nine** subsystems, and the documentation should treat this package
(`@oshun/platform-foundations`) as their canonical address rather than
scattering them:

| Subsystem (export)  | Path                                         | Responsibility                                                   |
| ------------------- | -------------------------------------------- | ---------------------------------------------------------------- |
| `service-discovery` | `src/service-discovery/service-discovery.ts` | Domain → service routing, health/readiness, deprecation          |
| `public-api`        | `src/public-api/oauth.ts`                    | §27 OAuth 2.1/PKCE, scoped tokens, quotas, audit                 |
| `shared-contracts`  | `src/shared-contracts/`                      | Cross-subsystem canonical shapes                                 |
| `role-model`        | `src/role-model/role-model.ts`               | §28 canonical roles, scopes, function-level authorization        |
| `step-up`           | `src/step-up/step-up.ts`                     | Step-up authentication for sensitive actions                     |
| `secrets`           | `src/secrets/secrets.ts`                     | Standardized secret loading, scoping, rotation                   |
| `configs`           | `src/configs/configs.ts`                     | Validated feature flags, policy bundles, env configs             |
| `rollback`          | `src/rollback/rollback.ts`                   | Safe rollback plans for shell/admin/grounding/persona/generation |
| `abuse-controls`    | `src/abuse-controls/abuse-controls.ts`       | Rate limits, payload caps, query-cost ceilings                   |

Related building blocks live in sibling shared libraries — the auth primitives
(`libs/shared/auth-primitives`), the HTTP client middleware
(`libs/shared/http-client`), the event bus (`libs/shared/event-bus`), the queue
substrate (`libs/shared/queue`), the inbound connectors
(`libs/shared/inbound-integrations`), data residency
(`libs/shared/data-residency`), and the audit platform
(`libs/shared/audit-platform`). The auth client package used by services is
`@oshun/auth-client` (`libs/oshun/auth`).

## The canonical domain registry

Every top-level domain is described once, in code, by the domain registry at
`libs/oshun/domain-registry/src/registry.ts`. `OSHUN_DOMAIN_IDS` enumerates the
six customer domains —

```
['tara', 'veritas', 'nyx', 'arete', 'nisaba', 'metis']
```

— and `OSHUN_SHELL_PRIMARY_DOMAIN` is `'tara'`, the domain the shell opens to.
`DOMAIN_REGISTRY` maps each id to a `DomainMetadata` record that carries far
more than a label: a `DomainAuthPolicy` (`required`, a `sessionKind` of
`customer | learner | reader | operator`, scopes, and `stepUpActions`), an
`'analytics-id'`, a `'notification-channel'`, an `'assistant-context-key'`, a
`'deep-link-prefix'`, a `'bff-base-path'`, a `DomainLaunchContract` with typed
permissions (`notifications | audio | location | calendar | camera | storage`),
a `DomainOfflineFallbackCard`, a `DomainShellNarrative`, and an
`'admin-taxonomy'` (`ownerSubsystem`, `primaryQueues`, `auditCategory`,
`contentClasses`). The BFF base paths are stable and uniform:

| Domain  | `bff-base-path`              |
| ------- | ---------------------------- |
| Tara    | `/api/oshun/domains/tara`    |
| Veritas | `/api/oshun/domains/veritas` |
| Nyx     | `/api/oshun/domains/nyx`     |
| Arete   | `/api/oshun/domains/arete`   |
| Nisaba  | `/api/oshun/domains/nisaba`  |
| Metis   | `/api/oshun/domains/metis`   |

Because a domain's route typing, auth/domain preferences, analytics enums,
notification preferences, assistant context key, deep-link prefix, and admin
taxonomy all originate from one record, the shell, the BFF, analytics, and the
admin console cannot drift out of agreement about what a domain _is_. Adding a
seventh domain means adding one entry, not editing eight subsystems.

## Shared contracts as the spine

V1's request and response shapes are canonical Zod contracts. They cover the
breadth the source enumerates — rituals, practices, concepts, passages, claims,
sources, notebooks, collections, continuity state, personas, voice profiles,
avatar packs, evidence packs, review packages, provenance bundles, consent
records, memory scopes, workflow templates, model cards, model versions, support
cases, incidents, and policy bundles — and they are the enforcement point for
object-property authorization, excessive-data-exposure, and mass-assignment
defenses (a request shape that doesn't model a field cannot smuggle it through).

Common contracts shared across domains live under `libs/contracts/src/common/`.
That directory contains **286 files** today — 161 implementation `.ts` modules
plus their colocated `.spec.ts`/`.test.ts` siblings. (An earlier doc figure of
"≈265 files" is stale and low; the exact total drifts up as contracts gain
tests, so treat the implementation-module count as the stable figure.)

The contracts index (`libs/contracts/src/index.ts`) does more namespace
re-exporting than the three commonly cited (`NisabaContracts`, `MetisContracts`,
`VeritasContracts`). It also namespaces the cross-version and living-scene
contract sets, using explicit aliasing where names would otherwise collide:

| Namespace export       | Source                 |
| ---------------------- | ---------------------- |
| `NisabaContracts`      | `./nisaba`             |
| `MetisContracts`       | `./metis`              |
| `VeritasContracts`     | `./veritas`            |
| `IrisContracts`        | `./iris/index`         |
| `AgentContracts`       | `./agent/index`        |
| `OyaContracts`         | `./oya/index`          |
| `V3Contracts`          | `./v3/index`           |
| `V6Contracts`          | `./v6/index`           |
| `V9Contracts`          | `./v9/index`           |
| `LivingSceneContracts` | `./living-scene/index` |

The living-scene set is aliased to avoid clashes (for example
`LivingSceneScoreSchema` and `deepParseLivingSceneScore`), so a consumer can
import scene contracts alongside domain contracts without symbol collisions. See
[Scene Score Schema](./scene-score-schema.md) for what those shapes model.

### The partial-failure envelope contract

When an operation fans out across domains (say, a bulk import touching Veritas
claims and Nisaba passages), some items can succeed while others fail. V1
represents that outcome as a **first-class Zod contract**, not an ad-hoc shape.
It lives at `libs/contracts/src/common/partial-failure-envelope.ts`:

```ts
// PartialFailureEnvelopeSchema (shape)
{
  results: unknown[],
  errors: PartialFailureError[],   // { domain, stage?, message }
  partial: boolean,
}
```

The module exports `PartialFailureEnvelopeSchema`, the per-error
`PartialFailureErrorSchema` (`domain` and `message` required, `stage` optional),
a generic factory
`createPartialFailureEnvelopeSchema(resultSchema, errorSchema?)` for typing the
`results`/`errors` arrays per call site, a constructor
`buildPartialFailureEnvelope(...)`, and the type guard
`isPartialFailureEnvelope`. The contract enforces two cross-field invariants via
`superRefine`, so an envelope cannot lie about its own state:

- `partial === true` requires **at least one** error entry, and
- `partial === false` requires the errors array to be **empty**.

`buildPartialFailureEnvelope` goes further and infers `partial` from whether any
errors are present, throwing if a caller passes a `partial` flag that disagrees
with the errors it supplied. The result is a uniform "here's what worked, here's
what didn't, and yes this was a partial outcome" envelope that every fan-out
caller can produce and every consumer can validate identically.

## Contract-to-persistence alignment

Canonical Zod schemas are kept honest against the Prisma data models by a real
alignment harness in `libs/oshun/persistence/src`. The centerpiece is the
introspection + rendering pair — `zod-prisma-introspection.ts` reads the Zod
contracts, and `prisma-renderer.ts` projects them — backed by the
drift/index/migration/tombstone **test quartet** the source promises:

| Test                                    | What it guards                                             |
| --------------------------------------- | ---------------------------------------------------------- |
| `contract-persistence-registry.test.ts` | Every persisted contract is registered and mapped          |
| `index-requirements.test.ts`            | Required query indexes exist for declared access patterns  |
| `migration-plan.test.ts`                | Schema changes produce a coherent, ordered migration plan  |
| `tombstone-semantics.test.ts`           | Soft-delete / tombstone behavior is correct and consistent |

Privacy deletion is implemented in the same library: `dsar-deletion-cascade.ts`
and `dsar-erasure-runtime.ts` (each with unit _and_ integration tests) carry the
DSAR (data-subject-access-request) cascade through the persistence layer, so an
erasure request actually propagates rather than merely flagging a row. Durable
stores for memory, snapshots, and the admin audit-events stream
(`durable-memory-store.ts`, `durable-snapshot-store.ts`,
`durable-admin-audit-events-store.ts`) provide the replayable critical histories
the source calls for — review state, approval state, memory, generation/research
jobs, persona release state, provenance bundles, audit events, and
review/incident timelines.

## Inter-service contracts: OpenAPI and proto

External and internal service contracts are published, not implicit.
`libs/openapi/src/specs/` holds **15 real spec directories** plus the aggregate
`main.yaml` and `v3.yaml`:

```
arete  bellona  calliope  concordia  hathor  isis  lilith
metis  nisaba   nyx       oshun-bff  sophia  tara  veritas  yemaya
```

For typed cross-service RPC, gRPC/proto is real and broad. `libs/proto` carries
a `buf.work.yaml`, a `generated/` tree, and roughly two dozen proto domain
directories under `src/` — `agent`, `ai`, `asset`, `auth`, `bridge`,
`collaboration`, `common`, `concordia`, `generation3d`, `health`, `hathor`,
`isis`, `loadbalancing`, `oshun`, `oya`, `pipeline`, `procedural`, `project`,
`reflection`, `rendering`, `shared`, `sophia`, `splatting`, and `user`. (The
older doc that named only Psyche/Isis/Sophia understated the proto footprint
substantially.)

## The public-API platform: OAuth 2.1 / PKCE

`libs/oshun/platform-foundations/src/public-api/oauth.ts` is the §27 public-API
authorization layer. To be precise about scope: this is a **contracts and
state-machine** implementation — token shapes, PKCE validation, refresh
rotation, revocation cascade, and quota evaluation — that the BFF and domain
services compose. It is not, by itself, a deployed live authorization server;
treat the deployment of an authorization-server endpoint as the integration step
around this verified core.

The module supports the grant types `authorization-code`, `refresh-token`, and
`client-credentials`, with two token types:
`TOKEN_TYPES = ['access', 'refresh']`. An `OauthClient` declares its
`clientKind` (`first-party | third-party`), `redirectUris`, `allowedScopes`,
whether it `requiresPkce`, separate access and refresh token lifetimes, a
`perKeyRateLimitPerMinute`, and an optional `tenantId` for tenant-scoped
clients.

PKCE is enforced structurally. The code challenge must match
`PKCE_CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43,128}$/` and the verifier its
own pattern, and only the `S256` challenge method is accepted —
`verifyPkceChallenge` recomputes the SHA-256 of the verifier (via
`@noble/hashes`) and compares. `validateAuthorizationCodeRequest` returns a
typed list of `AuthorizationCodeError`s rather than throwing, covering the real
failure modes:

| Error code                       | Meaning                                     |
| -------------------------------- | ------------------------------------------- |
| `client-mismatch`                | Request `clientId` ≠ registered client      |
| `invalid-redirect-uri`           | Redirect URI not in the client's allow-list |
| `scope-not-allowed`              | Requested scope outside `allowedScopes`     |
| `pkce-required`                  | Client requires PKCE but none supplied      |
| `invalid-code-challenge`         | Challenge fails the pattern                 |
| `unsupported-challenge-method`   | Method ≠ `S256`                             |
| `code-expired` / `code-consumed` | Authorization code reused or stale          |
| `pkce-verification-failed`       | Verifier doesn't hash to the challenge      |

Refresh and revocation are modeled honestly as a state machine.
`redeemRefreshToken(...)` rotates tokens: it rejects a non-refresh token
(`wrong-token-type`), an already-revoked token (`revoked-refresh`), or an
expired one (`expired-refresh`); on success it returns a fresh access token, a
fresh refresh token, and the now-revoked prior refresh — and it stamps the new
access token's `mintingRefreshTokenId` so lineage is traceable. That lineage is
what `revocationCascade(...)` walks: revoking a refresh token id revokes that
token **and every access token minted from it**. Quotas are evaluated by
`evaluatePublicApiQuota(...)` against per-key windows, and privileged API
activity is captured via `buildPublicApiAuditEvent(...)`. Tokens carry `scopes`,
`tenantId`, and `subjectUserId`, so authorization stays tenant-aware across the
BFF and domain services.

The auth-primitives library (`libs/shared/auth-primitives/src`) is richer than
"JWT and session primitives": it ships `api-key.ts`, `jwt.ts`, `session.ts`,
`oauth-client.ts`, `oauth-revoke.ts`, `token-refresh.ts`, `token-audit.ts`,
`totp.ts` (TOTP for step-up), `platform-roles.ts`, `tenant-isolation.ts`, and
`password.ts`, each with colocated specs.

## The canonical role and scope model

Access control is anchored by `CANONICAL_ROLES` in
`libs/oshun/platform-foundations/src/role-model/role-model.ts` — **ten** roles,
not the looser ~eight the prose previously implied. The exact set is:

```
customer, creator, support-agent, reviewer, moderator,
privacy-operator, model-operator, persona-operator,
tenant-admin, admin-leadership
```

Two of these deserve surfacing because the older prose hid them: `creator` is a
distinct canonical role (it adds the `creator.studio` scope on top of
`customer.self`/`customer.shell`), and `tenant-admin` is distinct from
`admin-leadership` (a `tenant-admin` gets `tenant.console` and tenant-scoped
audit, while only `admin-leadership` gets the cross-tenant and break-glass
scopes). The prose name `support` should read `support-agent` to match the code.

Scopes are explicit (`SCOPE_KEYS`) and mapped per role; `scopesFor(role)`
returns the scope set, and `filterFieldsByRole(...)` shapes responses by role.
The scope-to-role mapping is least-privilege by construction:

| Role               | Granted scopes                                                                       |
| ------------------ | ------------------------------------------------------------------------------------ |
| `customer`         | `customer.self`, `customer.shell`                                                    |
| `creator`          | `customer.self`, `customer.shell`, `creator.studio`                                  |
| `support-agent`    | `support.case.read`, `support.case.write`                                            |
| `reviewer`         | `review.queue.read`, `review.queue.decide`                                           |
| `moderator`        | `review.queue.read`, `review.queue.decide`, `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` |

Function-level authorization runs through `authorizeAction(...)`, which takes
the subject's roles, the `requiredScope`, and the subject/target tenant ids and
returns a typed `AuthorizationDecision` whose denial `reason` is one of
`role-not-permitted`, `tenant-isolation`, or `object-not-owned`. Crucially,
cross-tenant authorization (a non-null target tenant that differs from the
subject's) requires `admin-leadership` — so a tenant-admin cannot reach across
into another institution's data. Step-up authentication
(`platform-foundations/src/step-up/step-up.ts`, backed by `totp.ts`) gates the
sensitive actions a domain declares in its `stepUpActions`.

## The event bus — and a correction

Cross-domain integration runs over the event bus at
`libs/shared/event-bus/src/event-bus.ts`:
`export class EventBus implements IEventBus`, instantiated by the factory
`createEventBus(config: EventBusConfig)`. It uses
`import { Redis } from 'ioredis'` with separate publisher and subscriber
connections, and it ships the topic registry, outbound delivery, and
webhook-simulator pieces the source promises (the default registry is
`DEFAULT_EVENT_TOPIC_REGISTRY` in `topic-registry.ts`, with schema-versioned
topics and payload validation via `EventPayloadValidationIssue`).

**Accuracy correction.** `V1/DEPENDENCIES.md` historically labeled this an
"Event Bus (Redis Streams)" — the one outright-wrong technical claim in this
area, since corrected in the registry. The implementation does **not** use
native Redis Streams — there is no `XADD`/`XREAD`/`XREADGROUP`/`XGROUP`
anywhere. Per the module's own header, it is built from ordinary Redis
primitives:

- **`publish()`** serializes the envelope, stores it in a **TTL-bounded key**
  (default `DEFAULT_EVENT_TTL = 86400` seconds — the replay source), and fans it
  out to subscribers over **Redis pub/sub**. A separate HASH tracks which
  subscriptions/consumer-groups have already processed an event so
  crash-restarts don't redeliver acked work.
- **`ack()`** marks `(eventId, subscriptionId)` or `(eventId, group)` processed;
  unacked work is redelivered on the next `replayUnacked` sweep.
- **`nack(delay)`** and delayed `publish({ delay })` reschedule through a
  durable **sorted set named `scheduled`**; a scheduler loop
  (`DEFAULT_SCHEDULER_INTERVAL = 250` ms) pulls due entries — `setTimeout` is
  never the only copy, so it's crash-safe.
- **Consumer groups** are emulated: when a subscription declares a `group`,
  members race via **`SET NX`** on a per-`(eventId, group)` claim key, and only
  the winner runs the handler; without a group, every matching subscription runs
  (classic broadcast).
- **Dead-letter** entries live in a **list** (for ordered pagination) and a
  **hash** (for O(1) id lookup); `removeDeadLetter` uses `LREM` on the
  serialized entry for atomic removal.

So the BFF→…→event-bus→audit composition the architecture diagrams describe is
real and matches the building blocks present (idempotency → tenant → residency →
Zod → adapter → event-bus → audit). Only the "Redis Streams" substrate label was
wrong; the accurate description is "Redis pub/sub with TTL-keyed replay, a
`scheduled` sorted set, and `SET NX` consumer-group claims." (We have not traced
a single production BFF route proving every middleware fires in exactly that
order in deployed wiring — the components exist; the end-to-end order is a
composition claim.) Outbound webhook delivery rides on top with signing,
retry/backoff, dead-letter routing, replay, and delivery audit.

## The durable queue substrate

Cross-domain background jobs (editorial, asset, agentic AI, notification,
billing, integration) run on the queue substrate at `libs/shared/queue/src`,
which matches the source's "durable queues, replay, deduplication, priority
classes, observability" claim with real modules:

| Module                 | Role                                                   |
| ---------------------- | ------------------------------------------------------ |
| `durable-queue.ts`     | Persistent, replayable job queue with priority classes |
| `dead-letter-queue.ts` | DLQ for jobs that exhaust retries                      |
| `sla-monitor.ts`       | SLA tracking / observability over queued work          |
| `memory-queue.ts`      | In-memory queue (tests / single-process)               |
| `worker.ts`            | Worker loop that drains and acks jobs                  |

The durable queue, DLQ, and SLA monitor each ship colocated specs.

## Inbound integration connectors

Inbound integration plumbing lives in `libs/shared/inbound-integrations/src`,
whose `index.ts` exports **15 connector modules** — substantially more than the
nine-row table older docs showed. These are sizable, real implementations (for
example `oneroster.ts` is ~1081 LOC, `lms.ts` ~1425 LOC, and `identity.ts` ~1048
LOC):

| Connector module            | Covers                                                      |
| --------------------------- | ----------------------------------------------------------- |
| `types`                     | Shared connector descriptors and health types               |
| `lms`                       | LMS integration (LTI 1.3 / LTI Advantage surface)           |
| `lti-verification`          | LTI launch/JWT verification                                 |
| `scorm-rte`                 | SCORM run-time environment                                  |
| `scorm-2004-rte`            | SCORM 2004 run-time environment (distinct from `scorm-rte`) |
| `oneroster`                 | OneRoster rostering                                         |
| `identity`                  | SAML 2.0 / OIDC / SCIM 2.0 identity                         |
| `calendar`                  | Calendar integration (Google / Apple / Outlook)             |
| `calendar-google-transport` | Google-specific calendar transport                          |
| `payment`                   | Payment connector                                           |
| `telemetry`                 | Telemetry sinks (xAPI / cmi5 / Caliper)                     |
| `byom`                      | Bring-your-own-model ingestion                              |
| `byom-model`                | BYOM model descriptor (distinct from `byom`)                |
| `notification`              | Partner notification sinks                                  |
| `health`                    | Connector health reporting                                  |

Note the modules older tables omitted: the **two** SCORM RTEs (`scorm-rte` _and_
`scorm-2004-rte`), `lti-verification`, the separate `byom-model`, and
`calendar-google-transport`. Each connector carries version pinning and health
reporting, and the framework supports declarative descriptors, sandbox tenants,
certification flows, OpenAPI specs, code samples, and operator-managed
enable/disable per tenant. Metis is the primary consumer of
LMS/OneRoster/identity (see [Metis](./domain-metis.md)).

## HTTP-client middleware: idempotency, SSRF, breakers

The outbound HTTP client (`libs/shared/http-client/src`) carries the request
hygiene the architecture leans on:

- **Idempotency** — `idempotency.ts` exports
  `DEFAULT_IDEMPOTENCY_HEADER = 'Idempotency-Key'`,
  `DEFAULT_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000` (24h),
  `DEFAULT_IDEMPOTENCY_MAX_ENTRIES = 1000`, an `IdempotencyMiddleware` class,
  and `createIdempotencyMiddleware(...)`. It defaults to guarding mutating
  methods (`POST`/`PUT`/`PATCH`/`DELETE`), fingerprints the request, and throws
  typed errors — `IdempotencyKeyRequiredError`,
  `IdempotencyRequestMismatchError`, `IdempotencyWaitAbortedError`,
  `IdempotencyFingerprintError` — so a replayed key with a _different_ body is
  rejected rather than silently re-run.
- **SSRF guard** — `ssrf-guard.ts` blocks unsafe-upstream consumption for URL
  ingestion, fetch tools, webhooks, BYOM ingestion, and retrieval connectors.
- **Resilience** — `circuit-breaker.ts`, `retry.ts`, `timeout.ts`, plus
  `tenant-context.ts` (tenant-aware request scoping) and `tracing.ts`.

## Data residency

Residency enforcement is real, in `libs/shared/data-residency/src`:
`enforcer.ts` decides whether an operation may touch data in a given zone,
`home-zone.ts` models a tenant's home region, `traffic-shaping.ts` implements
residency-aware traffic shaping, `dsr-routing.ts` routes data-subject requests,
and an injected `ResidencyAuditPublisher` seam (exercised by
`audit-platform-bridge.spec.ts`) ties residency decisions into the audit chain.
Together these back the "multi-tenant routing, residency-aware traffic shaping,
tenant-aware caching/flags/experiments" guarantees the source lists.

## The audit platform — an append-only hash chain

Audit is not a single sentence; it is a large substrate
(`libs/shared/audit-platform/src`, ~40 implementation modules). The integrity
backbone is `hash-chain.ts`, which implements an **append-only, tamper-evident
hash chain**: `hashEvent(...)` hashes a `CanonicalPlatformAuditEvent`,
`chainHashStep(previousChainHash, eventHash)` links each entry to its
predecessor, and `HashChainedAuditEventStore` /
`createHashChainedAuditEventStore(...)` persist the chain. Verification
(`AuditChainVerificationResult`, `AuditChainMismatch`) detects breaks and raises
`AuditChainTamperDetectedError`, and `canonicalSerialize(...)` guarantees stable
hashing across runs. Around that core sit compliance and provenance modules,
including `schema-versioning`, `retention`, `escalation-rules`,
`support-escalation`, `compliance-attestation`, `watermark-verification`,
`synthetic-media-labeling`, `voice-likeness-consent`, `provenance-attachment`,
and `provenance-badges`. This is what gives the privileged-action workflows
(support, review, moderation, privacy, model, persona, copilot override,
incident) their immutable, per-flow audit assertions.

## Security as a launch gate

V1 is a multi-tenant, multi-role product handling consented memory, synthetic
media, educational records, and institutional rosters, so security is treated as
a **launch gate**, not post-launch hardening. The role/scope model, step-up
auth, audit hash chain, idempotency, SSRF guard, residency enforcement, and
abuse controls above are the implemented core. The broader program the source
enumerates — and the honest status of each control — is:

| Control area                                                                      | Status                                                           |
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Canonical role model, least-privilege scopes, step-up, tenant/env separation      | Implemented (`role-model`, `step-up`, `tenant-isolation`)        |
| Immutable audit + per-flow audit assertions                                       | Implemented (`audit-platform` hash chain)                        |
| Standardized secrets handling, rotation, scoping                                  | Implemented (`platform-foundations/secrets`)                     |
| Validated configs, policy bundles, experiment guardrails, rollback plans          | Implemented (`configs`, `rollback`)                              |
| Object-property auth, excessive-data-exposure, mass-assignment defense            | Implemented via canonical Zod shapes + `filterFieldsByRole`      |
| Resource-consumption / abuse controls (rate, payload, pagination, query-cost)     | Implemented (`abuse-controls`)                                   |
| SSRF / egress-policy / unsafe-upstream enforcement                                | Implemented (`ssrf-guard`)                                       |
| Permission/authorization regression suites, tenant-isolation/leakage suites       | Implemented (function-level `authorizeAction` + isolation specs) |
| Authenticated DAST against staging web/admin/APIs                                 | Process/CI gate — exercised against deployed staging             |
| SAST + CodeQL, dependency-vuln + secret scanning                                  | CI/process gate                                                  |
| SBOM + provenance verification + release signing                                  | Process gate, "where supported"                                  |
| Fuzz / malicious-input suites (uploads, markdown, search, tool/webhook surfaces)  | Process gate                                                     |
| API inventory / shadow-endpoint discovery per release                             | Process gate                                                     |
| Pre-GA pen-test + red-team (auth, RBAC, privacy, Metis, grounded gen, escalation) | External engagement, scoped + signed off                         |

The bottom rows are deliberately marked as process/CI/external gates rather than
"shipped code": DAST, pen-testing, SBOM signing, and red-team exercises are
launch activities run against deployed environments, and the docs keep that
candor rather than claiming a green checkmark for work that happens at release
time.

## Why it works this way

The recurring pattern across these foundations is **one canonical definition,
many consumers**: a single domain registry, a single contracts spine, a single
role/scope model, a single audit chain, a single idempotency policy. Drift is
the enemy of a multi-tenant, multi-domain platform — eight subsystems each
holding their own copy of "what a domain is" or "what a customer may do" is how
cross-account leakage and authorization gaps creep in. By making the source of
truth executable (and testing the alignment with the persistence quartet and the
authorization suites), the platform turns "did we keep these in sync?" from a
review question into a CI failure.

## Related

- [Subsystem Glossary](./glossary.md) — names and ownership for every subsystem
  referenced here
- [Privacy, Consent, Data Portability, and User Controls](./privacy-consent-portability.md)
  — DSAR cascade and consent records in practice
- [Channel Abstraction, Routing, and Boundaries](./channel-abstraction-and-boundary.md)
  — how channels sit above the BFF
- [Tenant, Institution, and Operator Toolkit](./tenant-toolkit.md) —
  operator-facing connector enable/disable and tenant scoping
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md) — the
  privileged workflows the audit chain covers
- [Analytics, Observability, Testing, and Security](./analytics-and-testing.md)
  — the testing and security program around these foundations
- [Metis — Education and Tutoring](./domain-metis.md) — primary consumer of the
  LMS/OneRoster/identity connectors
- [../features.md](../features.md) — feature hub
