The Tenant Toolkit is the per-tenant control plane of Oshun V1: the surfaces and
domain logic an institution, school, research lab, partner organization, or
managed cohort uses to run its own slice of Oshun — members, content scopes,
identity, custom roles, audit, bulk data, integrations, notifications, help, and
status. Each slice is strictly isolated from every other tenant and from Oshun's
own platform-operator world. The toolkit serves tenant administrators (a
district IT lead, a school's roster coordinator, a lab PI's delegate, a
partner's integration engineer) and, in a more privileged read role, Oshun
platform operators who investigate across tenants. Almost the entire toolkit is
shipped, real code: the rendering app apps/oshun/tenant-admin is a thin
Next.js layer, and the substance lives in the library
libs/oshun/tenant-console, whose nine modules are deterministic, well-typed,
and tested. Backlog tracking for the console substrate is §20 in
../TODOS.md.
Why a separate page from Admin Products. The operator-facing platform console and on-call mobile app are documented on Admin Products — Web and Mobile. That surface is platform-wide; this one is per-tenant. Both are thin Next.js shells over the same nine
@oshun/tenant-consolemodules — the toolkit described here is the tenant's half of that shared substrate, with hard isolation drawn between them.
Where the Logic Actually Lives#
The single most important architectural fact about this area — and one the
companion architecture docs understate — is that the tenant console is a
library, not an app. apps/oshun/tenant-admin is a standalone Next.js
application (the architecture glossary, high-level-architecture.md, and
product-surfaces.md all correctly place it there and tie it to §20), but it is
a rendering and BFF layer. The domain algorithms — SSO claim mapping, SCIM
diffing, role risk scoring, audit hash-chaining, bulk export serialization — all
live in libs/oshun/tenant-console, whose barrel src/index.ts re-exports
exactly nine modules:
| Module | Path | Responsibility |
|---|---|---|
tenant-model |
src/tenant-model/ |
Canonical tenant, hierarchy, policy inheritance, members/seats, content scopes, agentic defaults |
identity |
src/identity/ |
SSO, SCIM/OneRoster, federation, per-tenant auth policy |
roles |
src/roles/ |
Custom role templates, permission diff + risk scoring, dry-run harness, recertification |
audit-explorer |
src/audit-explorer/ |
Filtered, hash-chained, tenant-isolated audit access; redaction; export bundles |
bulk-ops |
src/bulk-ops/ |
Import dry-run + staged commit; multi-format export with integrity manifests |
integrations |
src/integrations/ |
API keys, webhooks, connector registry, sandbox/cert/openapi |
notifications |
src/notifications/ |
Multi-channel delivery decisions, templates, digest, suppression |
help-center |
src/help-center/ |
Contextual, role-aware help search, feedback, changelog subscriptions |
status-page |
src/status-page/ |
Component roll-up, banners, incidents, postmortems, SLA |
That apps/oshun/tenant-admin genuinely consumes the library is verifiable:
apps/oshun/tenant-admin/src/app/identity/page.tsx imports applyScimSync,
canFederateAuth, computeRosterDiff, evaluateAuthChallenge, and
processSsoLogin directly from @oshun/tenant-console. So the architecture
note that says "Oshun Tenant Console at apps/oshun/tenant-admin (standalone
Next app; §20)" is correct but under-attributed — the capability is the
library; the app is the view. This matters for reviewers: to verify a claim in
this page, read the library module, not the page component.
The runtime-boundary caveat (stated honestly)#
The identity functions are deterministic pure functions — there is no live
SAML XML parser or OIDC token validator inside the library. processSsoLogin
takes an already-parsed SsoLoginAttempt (asserted claims, a signature-valid
boolean) and computes the mapping/JIT/session result; it does not perform the
cryptographic assertion verification itself. The library is candid about this
seam: the SsoConnection.oidcClientId / oidcJwksUrl fields carry a doc
comment that the OIDC live login runtime fails closed (HTTP 503) when those
are absent, "so a config-only connection never pretends to be loginable." That
is a fail-loud seam in the no-fake-success sense, not a stub. The completeness
audit further notes that the tenant-admin config UI — metadata-XML /
OIDC-discovery parsing, the full transform matrix, the auth-policy panel,
sandbox-probe verdicts — is only partially covered end-to-end. Everything below
is real library logic; where the surrounding UI or live transport is partial, it
is called out.
Tenant and Organization Model#
A tenant is the canonical organizational entity. tenant-model/tenant.ts
defines
TENANT_KINDS = ['institution','school','class','cohort','lab', 'partner-org','district']
and a strict containment grammar via ALLOWED_CHILD_KINDS: a district may
contain school or institution; an institution may contain school, lab,
class, or cohort; a school contains class or cohort; a class
contains cohort; lab and partner-org contain only cohort; cohort is a
leaf. validateTenant enforces this grammar and also rejects self-parenting,
empty display names, an empty data-residency region, and negative/non-integer
seat allocations, returning a typed TenantValidationError[].
buildHierarchyChain walks parentTenantId upward, detecting 'cycle',
'unknown-tenant', and 'depth-cap-exceeded' against the
HIERARCHY_DEPTH_CAP = 6.
Onboarding metadata is concrete: TenantContractMetadata carries contractId,
billingEntityId, region, dataResidencyRegion, and a row of default-policy
pointers — defaultPolicyBundleId, defaultPersonaPolicyId,
defaultVoicePolicyId, defaultAvatarPolicyId,
defaultAgenticPipelineCatalogId, and an optional
defaultMetisInstitutionalConfigId. These are the seeds for everything a tenant
inherits — the persona/voice/avatar defaults feed
Lilith Persona Policy, and the Metis config feeds
Metis — Education and Tutoring.
Policy inheritance, scopes, members, and agentic defaults#
tenant-model/policy-inheritance.ts resolves a child tenant's effective policy
against its parent using
POLICY_MERGE_KINDS = ['override','merge', 'tighten-only']. The
'tighten-only' kind is the load-bearing one for multi-tenant safety:
resolvePolicy rejects a child overlay that loosens a tighten-only key,
emitting tighten-only-violation (or invalid-tighten-value on a type
mismatch, or unknown-key). A district can therefore set a floor a school
cannot relax.
tenant-model/content-scopes.ts governs what a tenant can see and use, over
SCOPE_OBJECT_KINDS = ['source','course','program','ritual','briefing', 'persona','voice-profile','avatar-pack','agentic-pipeline'],
with resolveScopeVisibility / bulkResolveScope applying override and
inheritance rules. tenant-model/members-and-seats.ts defines
MEMBER_STATUSES = ['active','invited','suspended','deactivated'], a
LicensePool, and allocateSeat / releaseSeat with typed
SeatAllocationErrors so seat accounting cannot silently overflow;
effectiveRoleIds resolves a member's roles across direct and group membership.
tenant-model/agentic-config.ts resolves a tenant's effective agentic pipeline
catalog, budgets, and gates via effectiveConfigFor, tying this surface to
Agent Registry, Job Orchestration, and Multi-Agent Plans
and
Agent Invocation, Budgets, Memory, and Feedback Loops.
Identity: SSO, SCIM, OneRoster, Federation, Auth Policy#
SSO (identity/sso.ts)#
SSO_PROTOCOLS = ['saml2','oidc']. A tenant configures one or more
SsoConnections, each carrying IdP entity id, endpoints, a signing-cert
thumbprint, allowed flows, JIT toggle, and session/refresh lifetimes. The heart
is processSsoLogin, which:
- Validates the clock and that
sessionLifetimeSecondsis positive andrefreshLifetimeSeconds >= sessionLifetimeSeconds. - Rejects an untrusted assertion signature (
signature-untrusted) and a flow the connection forbids (flow-not-permitted) —flowKindis'idp-initiated'or'sp-initiated', gated byallowIdpInitiated/allowSpInitiated. - Maps asserted claims through
SsoClaimMapping. Each mapping names anexternalClaim, aninternalAttribute∈{email, displayName, familyName, givenName, groups, tenant-role, preferred-locale}, arequiredflag, and atransform∈{identity, lowercase, csv-split, first-only}. A missing required claim yieldsmissing-required-claim. - Performs just-in-time provisioning when no
existingUserIdis present: ifjitProvisioningis off it returnsjit-disabled-and-user-not-found; if the mappedemailis missing/blank it returnsjit-email-missing-or-invalid; otherwise it mintsjit-${tenantId}-${email}and marksjitProvisioned.
On success it returns the resulting attribute map plus issuedAt,
sessionExpiresAt, and refreshExpiresAt as Unix seconds. The csv-split
transform splits and trims a comma-joined group string (common in SAML group
claims) into a clean array; first-only collapses a multi-valued claim to its
first element. As noted above, OIDC's live path fails closed (503) without
oidcClientId/oidcJwksUrl.
SCIM 2.0 and OneRoster (identity/scim.ts)#
SCIM_OPERATIONS = ['create','replace','patch','delete']. applyScimSync is a
pure decision function over a ScimSyncInput and the existing resource, with
ScimConflictResolution ∈ {reject, merge, idp-wins, local-wins}, returning a
verdict:
| Situation | Conflict policy | Verdict |
|---|---|---|
delete, resource absent |
— | noop |
delete, resource present |
— | applied (tombstoned: user active:false, group emptied) |
create, already exists |
reject |
rejected-conflict |
create, already exists |
idp-wins |
applied (incoming wins) |
create, already exists |
local-wins |
noop |
create, already exists |
merge |
merged (union of groups/emails/members, incoming scalars win) |
replace/patch, absent |
reject |
rejected-conflict |
replace/patch otherwise |
any | applied |
For Metis institutions, the same module models OneRoster rostering.
OneRosterClassResource and OneRosterEnrollmentResource both carry
status ∈ {active, tobedeleted, inactive} (the OneRoster status vocabulary),
with enrollments tracking role (student/teacher/administrator/aide/…),
primary, and begin/end dates. computeRosterDiff produces a dry-run preview:
it diffs incoming vs. existing users, groups, classes, and enrollments into
add/update/remove RosterDiffEntrys with an adds/updates/removes
summary, and surfaces referential conflicts — duplicate incoming
externalIds, and enrollments that reference an unknown class or unknown user.
This is the engine behind "OneRoster-grade rostering with periodic and on-demand
sync, dry-run preview, conflict reporting."
Federation and per-tenant auth policy (identity/federation-and-auth-policy.ts)#
canFederateAuth answers whether identities may flow from one tenant to
another, given a set of TenantFederationEdges. Each edge records
direction ∈ {one-way, mutual}, the approving actor, approval time, and a
policyBundleId. A mutual edge satisfies federation in both directions; a
one-way edge only from → to. This is how district ↔ school ↔ class
federation is expressed with explicit policy and audit, and how isolation is the
default (no edge → no federation).
evaluateAuthChallenge implements per-tenant authentication policy against a
TenantAuthPolicy:
mfaMethodsAllowed ∈ {totp, webauthn, sms, push}andmfaRequired;AUTH_STEP_UP_TRIGGERS = ['high-risk-action','export-sensitive-data', 'admin-override','incident-response','after-inactivity'];- a CIDR
ipAllowlist(empty = no restriction),requireDevicePosture,sessionRefreshSeconds, andmaxInactivitySeconds.
The evaluation order is deliberate and deny-first: an IP outside a non-empty
allowlist returns {verdict:'deny', reason:'ip-blocked'} immediately (a hard
network gate beats a step-up); then unmet MFA → step-up-required reason
'mfa'; then unattested device posture → 'device-posture'; then inactivity
beyond maxInactivitySeconds → 'inactivity'; then a matching step-up trigger
→ 'trigger'; otherwise {verdict:'allow'}. CIDR matching is a real bit-masked
IPv4 implementation (ipMatchesCidr/ipToInt), not a string prefix check.
Custom Roles, Permission Diffs, and the Dry-Run Harness#
roles/roles.ts layers tenant-editable role templates on a canonical model and,
critically, scores the risk of every deviation. The doc mentions a "test
harness" only generically, so the concrete primitives are worth naming.
diffTemplateAgainstCanonical compares a TenantRoleTemplate against its
CanonicalRole and emits a PermissionDiffEntry per capability with a
delta ∈ {unchanged, granted-by-tenant, revoked-by-tenant, scope-narrowed, scope-widened}
and a per-entry risk weight:
| Delta | Risk weight | Meaning |
|---|---|---|
revoked-by-tenant |
RISK_WEIGHT_REVOKE = 1 |
Tenant removed a capability the baseline grants |
scope-narrowed |
RISK_WEIGHT_SCOPE_NARROW = 1 |
Tenant tightened a scope restriction |
granted-by-tenant |
RISK_WEIGHT_GRANT = 3 |
Tenant added a capability the baseline lacks |
scope-widened |
RISK_WEIGHT_SCOPE_WIDEN = 4 |
Tenant broadened (or made disjoint) a scope — the most dangerous |
unchanged |
0 | No deviation |
The entries sum to a riskScore; the report sets
requiresReviewerSignoff = true when the canonical role isElevated or the
score meets REVIEWER_SIGNOFF_THRESHOLD = 6.
validateTemplateWithinApprovedBounds then rejects unapproved capability grants
(unapproved-capability-grant) and scope wideners (unapproved-scope-widening)
outside an operator-approved set, and emits reviewer-signoff-required when
signoff is needed but no reviewerSignoffActorId is supplied. Note the
conservative scope comparator: disjoint or partially-overlapping restriction
sets are treated as 'wider', because a set that is not a strict subset of the
canonical is more permissive than a strict subset would be.
effectiveCapabilities resolves a template's inheritance chain
(inheritsFromTemplateIds) by topological order, applying overrides leaf-last
so the most specific template wins.
Recertification state machine#
ASSIGNMENT_STATES = ['pending','approved','active','expired','recertified', 'revoked'].
transitionAssignment enforces a legal transition table
(pending → approved|revoked, approved → active|revoked,
active → expired|recertified|revoked, expired → recertified|revoked,
recertified → expired|revoked, revoked → ∅), plus two guardrails: an
approval cannot be self-served (approver-required when the actor is the
requester), and you cannot move into active/recertified past
expiresAtUnixSeconds (past-expiry). This is the workflow behind "role
assignment with approval, expiration, and recertification."
The role test harness#
runDryRun is the literal "dry-run a role against representative flows"
feature. Given a template's effective capabilities and a list of
DryRunActions — each {capabilityId, scope, expected:'allow'|'deny'} — it
computes the actual verdict (deny if the capability is absent/disallowed, or
if a scope restriction exists and the action's scope is null or not in the
allowed set; otherwise allow) and returns DryRunResults with matched.
Mismatches are exactly the "unexpected escalations" an admin surfaces before
deploying a role change.
Audit Log Explorer (audit-explorer/audit-explorer.ts)#
Each AuditLogRecord carries actor, role, action,
severity ∈ {info, warning, critical}, timestamp,
targetObjectClass/targetObjectId, an optional policyBindingId and
correlationId, an opaque payload, and — the mechanism the doc abstracts as
"tamper-evident storage" — a contentDigest plus a priorDigest. Those two
fields form a per-tenant hash chain: each record's priorDigest must equal
the previous record's contentDigest. verifyAuditChain walks a segment and
returns {valid:false, breaksAtRecordId} at the first link that does not match,
so any insertion, deletion, or mutation is detectable. (The library computes the
digest with a caller-supplied secret key — it does not invent crypto inside the
pure function.)
applyAuditFilter filters by actor id, actor role, tenant id, action, severity,
time window, target object class, policy binding, and correlation id — and is
viewer-scoped: an ExplorerViewer is
{role:'platform-operator' | 'tenant-admin', tenantId}, and a tenant-admin
viewer can never see records outside its own tenantId regardless of filter.
That scoping is the hard isolation boundary between tenant audit and Oshun-only
operator audit.
Supporting machinery: buildCorrelationThread groups records sharing a
correlationId into a time-sorted thread (cross-event correlation for
support/security/DSAR investigations); applyRedactions applies
RedactionRules (mode ∈ {remove, mask, pseudonymize}, optionally scoped to
target object classes) to nested payload fields during export and records what
was redacted; SavedInvestigation (with a hashed shareable token) and
AuditExportBundle (with a chainOfCustody of prior-bundle id, initial/final
digest, and retention expiry) back saved/shareable investigations and
retention-aware export bundles.
Bulk Operations and Data Import/Export#
Import dry-run and staged commit (bulk-ops/bulk-ops.ts)#
BULK_RESOURCE_KINDS = ['users','rosters','classes','courses','content', 'sources','taxonomy','persona-assignments','entitlements']
and BULK_OP_KINDS = ['import','export','action']. dryRunImport validates
each BulkRow against a list of RowValidators (typed BulkValidationError
codes: missing, invalid-type, out-of-bounds, duplicate,
conflict-with-existing), counts in-batch duplicate externalIds, classifies
rows as adds vs. updates against existingExternalIds, and — importantly —
counts residency violations: residencyViolationCount increments whenever a
row's residency region differs from the tenant's.
BulkDryRunReport.readyToCommit is true only when there are zero errors and
zero residency violations. This is residency-aware import that cannot silently
cross a region boundary.
planStagedCommit applies rows in order, recording committed / failed / skipped
ids and a resumeFromRowIndex so an operator can resume after a partial
failure, and synthesizes per-row audit event ids. gateBulkAction enforces
safety on selection-set actions: it blocks with rationale-required on an empty
rationale and exceeds-safety-limit when the selection size exceeds the
action's safetyLimit — the "safety limits, rationale capture" promise made
real.
Multi-format export with per-resource constraints (bulk-ops/bulk-export.ts)#
BULK_EXPORT_RESOURCE_KINDS = ['content','rosters','users','audit','rights', 'metis']
and BULK_EXPORT_FORMATS = ['json','csv','oneroster','xapi', 'caliper']. The
doc flattens these into a single "format options" list, but the code enforces a
per-resource-kind support matrix (BULK_EXPORT_FORMAT_SUPPORT, checked by
isBulkExportFormatSupported):
| Resource kind | Allowed formats |
|---|---|
content |
json, csv, oneroster |
rosters |
json, csv, oneroster |
users |
json, csv, oneroster |
audit |
json, csv, xapi, caliper |
rights |
json, csv |
metis |
json, csv |
executeBulkExport fails loud with format-not-supported outside this matrix,
and also guards mismatched-resource-kind, residency-violation (a record
whose region differs from the export target), empty-export, and
invalid-chunk-size. It chunks records (maxRecordsPerFile, default 5000),
serializes each chunk deterministically (records sorted, fields canonicalized;
real xAPI 1.0.3 statements and IMS Caliper 1.2 events for audit, OneRoster 1.1
CSV column sets for users/rosters/classes), and emits a BulkExportManifest
(manifestVersion:'oshun-bulk-export-v1') with a SHA-256 hash per file and over
the canonicalized manifest. The hash function is dependency-injected so the
pure module can be tested deterministically and backed by node:crypto at
runtime; verifyManifest re-derives every file hash, byte length, and record
count, plus the manifest hash, to validate an export's integrity end-to-end.
These are the "integrity manifests" the doc names.
API Keys, Webhooks, and Outbound Integrations#
API keys (integrations/api-keys.ts)#
An ApiKey stores a prefix and a tokenHash — the full key is never
stored. authenticateApiKey rejects with a typed reason ∈
{unknown-key, revoked, expired, ip-not-allowed}: an unknown key or a
token-hash mismatch both return unknown-key (no oracle distinguishing the
two); a revoked key returns revoked; an expired key
(expiresAtUnixSeconds <= now) returns expired; a non-empty ipAllowlist
that the caller's IP misses returns ip-not-allowed (same real CIDR matcher as
auth policy). evaluateApiKeyRateLimit implements a fixed 60-second window
against rateLimitPerMinute (treating an invalid limit as exhausted, and a
null limit as unlimited). rotateApiKey mints a successor linked by
rotatedFromKeyId and puts the prior key into a grace expiry; revokeApiKey
stamps revokedAtUnixSeconds.
Webhooks (integrations/webhooks.ts)#
WebhookDelivery.status ∈ {pending, in-flight, success, failed, dead-letter}.
matchesTopicPattern supports three pattern forms: an exact match, a '.*'
suffix prefix-match (billing.* matches billing and billing.invoice.paid),
and a global '*'. subscriptionsForEvent selects active, same-tenant
subscriptions whose patterns match the event topic. applyDeliveryTransition
encodes the retry semantics: a success closes the delivery; a
transient-failure retries with nextBackoffSeconds (exponential growth,
capped at maxBackoffSeconds, with jitter) until attemptNumber >= maxRetries,
at which point it becomes dead-letter; a permanent-failure goes straight to
failed. replayDelivery resets a delivery to pending at attempt 1 — the
dead-letter replay tool. Response bodies are excerpted to 200 chars for the
audit record.
Connector registry (integrations/connector-registry.ts)#
CONNECTOR_KINDS = ['lms','calendar','identity','payment','telemetry', 'byom-ingest','slack','teams'].
The features prose over-enumerates relative to this taxonomy — it lists "LMS
connectors (LTI 1.3, LTI Advantage, SCORM …)" as if those were distinct registry
kinds. They are not: LTI 1.3, LTI Advantage, and SCORM are sub-flavors of the
single lms kind, not separate ConnectorKinds. The eight kinds above are the
real coarse taxonomy; treat the detailed protocol list as aspirational
sub-typing inside lms.
Each ConnectorRegistration carries a pinned version, available versions, an
error budget per hour, and a circuit breaker. evaluateCircuitBreaker opens
(budget-exhausted) when errors in the current hour reach errorBudgetPerHour;
recordConnectorError rolls the hourly window and trips the breaker;
upgradeConnector refuses unknown-version and version-deprecated, making
good on the "version pinning and upgrade pathways" promise.
Developer portal — under-described in the source#
The integrations section says only "OpenAPI specs, code examples, sandbox
tenants, webhook simulators, and integration certification flows." The real
substrate is richer. libs/oshun/developer-portal/src/index.ts exports five
modules: openapi-builder, code-example-generator, sandbox-tenant-policy,
certification-suite, and developer-docs-registry.
openapi-buildergenerates an OpenAPI 3.1 document from declarative endpoint specs, converting Zod schemas viazodToOpenApiSchema(callers can override with hand-written JSON Schema where Zod isn't 1:1). Endpoint specs carryauth ∈ {public, bearer-developer-key, bearer-admin-session},idempotent, andrateLimitPerMinute.certification-suiteships a concrete check catalog.CERTIFICATION_CHECK_KINDSincludesauth-token-rotation,webhook-signature-verification,webhook-replay-rejection,rate-limit-respect,idempotency-key-honor,pagination-cursor-correctness,partial-failure-envelope-parse,residency-region-honor,event-deduplication,oauth-pkce-on-public-client,scopes-least-privilege, andprivacy-deletion-fanout. EachCertificationCheckhas agateLevel ∈ {required, recommended, optional}and a remediation URL;computeCertificationVerdictproduces the suite verdict.
These are surfaced through the admin app at api/admin/developer-portal/*
(articles, certification checks, sandbox policies).
Notifications, Lifecycle Communications, and Templates (notifications/notifications.ts)#
NOTIFICATION_CHANNELS = ['in-app','push','email','sms','voice','webhook'] —
and yes, voice is a real enum member, not aspirational copy. Severities
are NOTIFICATION_SEVERITIES = ['info','standard','important','critical'].
decideDelivery resolves preferences with an overlay precedence — per-persona
override, then per-domain override, then base perChannel — and emits a
DeliveryDecisionEntry per channel with one of: deliver, skip-disabled,
skip-quiet-hours, skip-below-severity, skip-bounced, skip-suppressed,
defer-to-digest. The rules encode real edge cases: a critical notification
breaks through quiet hours and is never deferred to digest;
digestMode ∈ {off, hourly, daily, weekly} only defers non-critical email.
renderTemplate enforces locale fallback, missing-variable, and
template-not-approved (a template must be in approved state to render), and
isUserSuppressedForChannel honors unsubscribe/bounce/compliance/spam
suppression records, channel-specific or global. Sibling files in the module
handle A/B testing (ab-testing.ts), digests (digest.ts), and the customer
message center (message-center.ts).
Help Center and Knowledge Base (help-center/help-center.ts)#
HELP_AUDIENCES = ['customer','admin','creator','partner'] and
HELP_KINDS = ['article','walkthrough','video-script','faq','changelog'].
searchHelp is role- and context-aware: it skips unpublished/archived
articles and articles whose audience or applicable-role set the viewer doesn't
satisfy, scores a term match (title hits are weighted higher), and adds a
contextual bonus when the article's applicableDomains includes the current
domain or one of its applicableSurfacePaths prefixes the current surface — so
contextual help surfaces even with an empty search term. aggregateFeedback
rolls up helpful/unhelpful counts and "what's missing" subject counts (feeding
the editorial backlog — see
Editorial Calendar and Asset & Media Library),
and shouldDeliverChangelog filters "what's new" by
minSeverity ∈ {patch, minor, major} and locale.
System Status, Maintenance, and Public Communications (status-page/status-page.ts)#
COMPONENT_STATES = ['operational','degraded','partial-outage','major-outage', 'maintenance'].
rollUpComponentStates reduces a component set to its worst state by a fixed
rank (operational < maintenance < degraded < partial-outage < major-outage),
and buildStatusPageView computes overall status, a per-region worst-state map,
active (unresolved) incidents, and time-sorted upcoming maintenance. An
IncidentRecord carries severity ∈ {minor, major, critical} and a list of
publicUpdates whose
status ∈ {investigating, identified, monitoring, resolved} — the standard
incident-comms lifecycle. activeBannersFor targets banners by audience tenant
ids, audience roles, time window, locale (with fallback), and per-viewer
dismissal; postmortemVisibleTo enforces
visibility ∈ {public, tenant-scoped, internal} (an internal postmortem is
visible only to a platform-operator). status-page/sla.ts and
subscriptions.ts round out SLA-breach and subscribe-by-channel handling.
The Apps: Tenant-Admin Web and the Admin BFF Surface#
apps/oshun/tenant-admin is the tenant-facing Next.js app; its
src/app/identity/page.tsx imports the identity functions named above directly
from @oshun/tenant-console, confirming the app is a genuine consumer of the
library rather than a parallel reimplementation.
The platform-operator app apps/oshun/admin (274 files under src/components/,
with Admin Products — Web and Mobile covering it in
depth) exposes a concrete BFF route surface under src/app/api/admin/ that this
area's architecture description never enumerates. Representative handlers
include signin and signout; search; notifications (plus
[notificationId]/seen, /dismiss, mark-all-seen); bulk-operations (and
[operationId]/transition); integrations/api-keys (and /revoke),
integrations/snapshot, integrations/connectors/upgrade,
integrations/webhooks/simulator; agentic-operations/kill-switches and
agentic-operations/snapshot; editorial/release-streams; the
audit/bulk-export families (audit-log/events, audit-log/investigations/*,
bulk-exports/*); communications/* (banners, incidents, help-articles,
status-components); and developer-portal/*. These are real route handlers, not
placeholders.
The on-call companion apps/oshun/admin-mobile is a real Expo app (app.json,
eas.json) with modules including urgent-queue, review (queue / routing /
decision / evidence clients), auth (a session controller with step-up and
expoSecureKeyValueStorage), incidents, offline, and notifications (an
Expo escalation router). It is detailed on
Admin Products — Web and Mobile.
What Is Real vs. Spec-Only#
| Capability | Status |
|---|---|
| Tenant model, hierarchy grammar, policy inheritance, scopes, seats | Real (tenant-model/*) |
SSO claim mapping + JIT + flow gating (processSsoLogin) |
Real pure-function core; live SAML/OIDC parser is a runtime seam |
| OIDC live login | Fail-closed 503 without oidcClientId/oidcJwksUrl (honest seam) |
SCIM 2.0 + OneRoster diff (applyScimSync, computeRosterDiff) |
Real |
Cross-tenant federation (canFederateAuth) |
Real |
Per-tenant auth policy + step-up (evaluateAuthChallenge) |
Real |
| Role diff, risk scoring, dry-run, recertification | Real (roles/roles.ts) |
| Hash-chained, tenant-isolated audit + redaction + export | Real (audit-explorer/*) |
| Bulk import dry-run, staged commit, multi-format export + manifests | Real (bulk-ops/*) |
| API keys, webhooks, connector registry | Real (integrations/*) |
| Developer portal: OpenAPI 3.1, certification suite, sandbox | Real (developer-portal/*) |
| Notifications, help center, status page | Real (3 modules) |
| Tenant-admin SSO config UI (metadata-XML / OIDC-discovery parse, full transform matrix, auth-policy panel, sandbox-probe verdicts) | Partial end-to-end UI coverage |
Nothing in this area reads as fabricated; the only honest caveats are at the runtime boundary (pure functions and fail-loud live seams) and in the partial config-UI coverage.
Related#
- Admin Products — Web and Mobile — the platform-operator
web and mobile apps that share the same nine
@oshun/tenant-consolemodules. - Privacy, Consent, Data Portability, and User Controls — DSAR/export and consent surfaces that the audit explorer and bulk export feed.
- Review, Compliance, and Trust & Safety — the governance and review machinery behind role signoff and incident handling.
- Support, Entitlements, Billing, and Customer Operations — entitlements and seat/billing context for the tenant model.
- Metis — Education and Tutoring — the institutional domain that OneRoster rostering and the Metis institutional config serve.
- Lilith Persona Policy — where the tenant's default persona/voice/avatar policies resolve.
- Architecture, Platform Foundations, and Security — platform-wide isolation, hash-chain, and security context.
- Subsystem Glossary and Product Surfaces — placement of the tenant console among V1 surfaces.
- Hub: ../features.md. Backlog: §20.