Shared tenancy contracts
@tsg-dsp/tenant-dtos defines the version 1 contracts used by the SSO Bridge,
Wallet, Control Plane and Tenant Management service. A tenant is an
organization/workspace; users can belong to several tenants. Configuration and
authorization revisions are independent, so membership updates do not regenerate
Wallet keys or participant identity.
Identity and administration
Canonical tenant UUIDs are shared across services. Existing domain foreign keys
continue to reference each service's local tenant ID. The additive Wallet and
Control Plane migrations leave canonicalTenantId null for existing rows.
TenantContextResolver.bindCanonical(localId, canonicalId) imports an explicitly
confirmed mapping without changing local IDs. Mappings are unique, immutable,
and reserved after deletion. Do not pair organizations using matching slugs
alone.
Principals are identified by the exact (issuer, subject) pair. Authorization
projections carry explicit local subjects and ownership aliases, including legacy
user:<id> aliases where applicable. Never merge identities by username or email.
Slugs are immutable and never recycled; names remain editable. Deleted tenants
retain their reserved canonical identity. Startup configuration never creates
or restores a tenant.
| Tenant operation | Allowed tenant roles |
|---|---|
| Read details/members | owner, admin, member |
| Rename/manage ordinary members | owner, admin |
| Delete/transfer ownership | owner |
Wallet, Control Plane and SSO consume managed state. Tenant Management owns all organization, membership, invitation and grant mutations. Organization roles remain separate from receiving-service permissions. Replacing or clearing a service grant requires authority over both its existing permissions and the replacement. Ownership transfer does not add service permissions. Only an explicitly registered platform administrator can recover the current owner's operator-profile baseline.
Target-local subjects and ownership aliases must stay unique across principals.
Headers and paths select tenants but do not authorize access. Conflicting
selectors fail with 400. X-Tenant-Id selects a local service ID; the OAuth
tenant_id claim carries the canonical UUID. Existing local rows retain their
identity, but protected access requires an explicit managed import and current
authorization. Disabled authentication and local seed reconciliation no longer
provide a tenant access path.
Actor identity and authority
The authenticated actor contract has two values:
actor_type | Principal | Tenant access |
|---|---|---|
human | A person, including browser sessions and human API automation | Active membership, service grant and principal mapping |
service | A machine identity, including tenant workloads and Tenant Management | Explicit service grant and principal mapping; an existing revoked membership still denies access |
The OAuth client identifies the application used to obtain a token; it does not
make a human caller a service. Browser sessions always normalize to human.
API access tokens must carry an explicit actor_type of human or service.
Client identifiers and grant names do not infer the actor kind. Missing or
unknown actor values are rejected.
Actor kind grants no platform authority. Tenant Management is a service whose
exact issuer/subject must be registered for each receiving service, audience and
provisioning operation. An ordinary service cannot provision merely by using
actor_type: service. Human platform administrators remain human; global
administration requires an explicit operator policy, independent of tenant roles.
OAuth client policies can distinguish shared browser clients, tenant workloads
and operator-controlled clients without introducing additional actor types.
SSO issues human/service claims and derives global service authority from its
registered client policy. Actor kind alone never permits provisioning.
Authorization projections
TenantProjectionModule supplies persistent snapshots to Wallet and Control
Plane. Each snapshot describes one (tenantId, serviceInstanceId) target.
Tenant Management delivers the same authorization revision/content and deadline
to the target service and to SSO. SSO uses the target's snapshot to issue tokens;
the target uses its copy to enforce current authorization. Wallet and Control
Plane receive only their own snapshots. SSO keeps separate copies for the
registered audiences it serves; there is no separate aggregate SSO grant format
and no second membership authority.
TenantProjectionService.apply checks a snapshot against an explicitly selected
target. Consumers pin that target to their own service instance. SSO must first
check that the target belongs to the tenant's registered service bindings, after
authenticating the provisioning caller. A payload's serviceInstanceId alone
never authorizes delivery. Database uniqueness and compare-and-swap updates
prevent concurrent or reordered delivery from restoring an older revision.
Same-revision content changes conflict. A same-content heartbeat can extend
validUntil within the 60-second freshness bound; replaying an old snapshot
cannot extend its original deadline.
principalMappings.localSubject and ownership aliases remain local to the target
service even in SSO's copy. For example, one (issuer, subject) can map to
legacy-wallet-user in Wallet and legacy-cp-user in Control Plane. SSO resolves
the authenticated login identity using the shared principal and issues from its
target grant; it must not require a Wallet or Control Plane local subject to
equal its own login ID. Only the SSO-targeted snapshot uses SSO-local mappings.
requireTenantAccess checks the verified actor's canonical tenant, single service
audience, current authorization revision, membership and bounded service grant.
It returns the explicit principal mapping for local ownership/resource checks.
Apply existing ABAC and repository scoping using the resolved local tenant
ID; this helper does not replace those checks. Services require explicit grants
and mappings, and revoked memberships remain denied.
Snapshots expire within 60 seconds and inactive tenants fail closed. Consumers must receive authenticated heartbeats even when entitlements are unchanged; manager outages therefore stop protected managed operations after expiry. Public DID/credential-status verification uses separate lifecycle rules. After every consumer durably acknowledges deletion and the current operation succeeds, the manager retires authorization renewal and pending delivery work for that tenant. Its tombstone and reserved identity remain; incomplete deletion continues retrying until acknowledged.
Provisioning callers must satisfy requireProvisioningAccess with an
operator-registered issuer/subject, receiving service instance, audience and
operation. Ordinary tenant admins and delegation headers cannot satisfy that
policy. Configuration validation, durable operation receipts, outbox delivery,
reconciliation and managed endpoint activation are supplied by Tenant Management.
All managed consumers require a running manager to renew their authorization
snapshots.
Managed browser authorization
The managed consumer integration keeps the global login session separate from API authorization. The session establishes the user's identity; an API access token authorizes that principal for one selected canonical tenant and one service audience. An ID token or a session's copied permission array does not authorize managed tenant operations.
The application backend retains verified access/refresh tokens in server-side session storage, associated with the issuer, subject, client, canonical tenant and audience. Tokens must not be exposed through user-info responses, browser storage or logs. On login/callback, verify ID and access tokens separately and require them to identify the same principal. On each cookie-authenticated API request, select the correctly bound access token, validate it, then run the same fresh projection, tenant, audience, revision and resource checks as for a bearer request. Clear tokens on logout or identity changes.
Selecting another tenant obtains a new authorization-code grant for that tenant and audience using the existing SSO login session where possible. A selector cannot retarget a token. Refresh rotates credentials for the original binding; it cannot change tenant/client/audience or increase the granted scope. Serialize concurrent refreshes for a binding so normal parallel requests do not trigger refresh replay handling. Missing, expired or stale authorization must be renewed or denied; never fall back to the global session or a service's own credentials.
SSO enforces tenant/audience-bound issuance, client permission ceilings and
code/refresh restrictions. Its local administration can resolve session
grants against SSO's own snapshot; that does not make its global session an API
credential for Wallet or Control Plane. Server-held tokens authorize
consumer callbacks, guards and tenant switchers, with explicit per-request
tenant selection so multiple tabs and late responses cannot cross tenants.
Shared authentication defaults to tenancy.mode: managed; Tenant Management
uses authority for global identity login. Wallet and Control Plane run managed
consumer authorization, while SSO always issues from managed client policies.
Authentication and upgrade
API JWTs require type at+jwt or application/at+jwt, expiry, subject, issued-at,
issuer, explicit actor_type and the receiving audience. auth.expectedAudience defaults to
auth.clientId. Opaque tokens require an active introspection result with
subject, issuer, audience, expiry and an explicit access-token type. JWT
verification failures never fall back to introspection. Browser login validates
ID tokens separately against the issuer and browser client; an ID token never
authorizes an API request.
Wallet, Control Plane and SSO no longer start legacy seeded configurations.
Register the manager, explicit client kinds and service audiences before the
upgrade. Supply stable SESSION_SECRET and TENANCY_SECRET values to every
browser-facing service; replicas must use the same values and durable session
storage. Back up the encryption secret together with the databases.
Import existing relationships and service-specific grants explicitly. Preserve published URLs, DIDs and keys, and resolve ambiguous owners with the operator. Existing local rows do not automatically become managed organizations. The deployment and migration guide describes explicit local URL/identity import, pending invitations, rollback, retention and the availability cost of expired snapshots.
Execution lease expiry
Updated HTTP and analytics workers return a typed HTTP 410 response for an owner-scoped lease that has expired or been suspended. The Control Plane conditionally suspends the still-started dataflow and its still-started transfer, clears the stale access address and records a transfer event. A conflict or an unknown owner does not trigger this reconciliation. Deploy both the worker and Control Plane changes to enable it; older worker responses use bounded backoff. Reconciliation preserves history and never resumes an expired grant. Healthy analytics grants remain active after a job completes so result downloads can continue.