Skip to main content
Version: v0.21.0

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 operationAllowed tenant roles
Read details/membersowner, admin, member
Rename/manage ordinary membersowner, admin
Delete/transfer ownershipowner

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_typePrincipalTenant access
humanA person, including browser sessions and human API automationActive membership, service grant and principal mapping
serviceA machine identity, including tenant workloads and Tenant ManagementExplicit 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.