Sylin.Koan.Identity.Tenancy
1.0.74
dotnet add package Sylin.Koan.Identity.Tenancy --version 1.0.74
NuGet\Install-Package Sylin.Koan.Identity.Tenancy -Version 1.0.74
<PackageReference Include="Sylin.Koan.Identity.Tenancy" Version="1.0.74" />
<PackageVersion Include="Sylin.Koan.Identity.Tenancy" Version="1.0.74" />
<PackageReference Include="Sylin.Koan.Identity.Tenancy" />
paket add Sylin.Koan.Identity.Tenancy --version 1.0.74
#r "nuget: Sylin.Koan.Identity.Tenancy, 1.0.74"
#:package Sylin.Koan.Identity.Tenancy@1.0.74
#addin nuget:?package=Sylin.Koan.Identity.Tenancy&version=1.0.74
#tool nuget:?package=Sylin.Koan.Identity.Tenancy&version=1.0.74
Sylin.Koan.Identity.Tenancy
Koan's durable Identity × Tenancy bridge. Reference it when an authenticated person should enter a tenant only through
a current membership. The package composes request scoping, tenant-role projection, access explanation, and lifecycle
closure through the application's existing AddKoan()—there is no bridge-specific setup call.
Install
dotnet add package Sylin.Koan.Identity.Tenancy
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKoan();
var app = builder.Build();
await app.RunAsync();
The application also needs its ordinary Web Auth connector and a Data provider. Identity and Tenancy arrive through the bridge package; add them directly only when application code intentionally uses their public types.
Meaningful behavior
Create a tenant seat as business data:
await new Membership
{
TenantId = tenant.Id,
IdentityId = person.Id,
Roles = { "orders:review" }
}.Save();
An authenticated request can now select that tenant by the default tenant claim, X-Koan-Tenant header, or
/t/{tenantCode} path. The bridge verifies the durable person is active and the membership still exists, establishes
Tenant.Current, and projects only that membership's tenant roles for the rest of the request. A forged carrier,
anonymous caller, inactive person, or removed seat proceeds unscoped; tenant-managed operations then fail closed.
Subdomain routing is available but inert until the application's base hosts are configured.
Configuration
{
"Koan": {
"Data": {
"Tenancy": {
"Resolution": {
"ClaimType": "tenant",
"HeaderName": "X-Koan-Tenant",
"PathPrefix": "/t/",
"BaseHosts": [ "app.example.com" ]
}
}
}
}
}
Carrier order is claim, header, subdomain, then path; the first resolved candidate wins. Every carrier always requires an active durable member. Invalid empty carrier settings fail host startup through standard .NET options validation. Startup reporting lists the effective carriers and says whether subdomain routing is live.
Lifecycle closure
DeprovisioningService.RemoveFromTenantAsync(personId, tenantId) removes one seat. DeactivateAsync(personId) marks
the person deactivated first, revokes all Koan cookie sessions, then removes every tenant seat. Both return a
DeprovisioningReceipt whose content hash can detect later changes with HasValidHash().
These operations are ordered, idempotent Entity writes—not a cross-provider transaction. A receipt is emitted only after the requested workflow completes and attests only to its own recorded fields.
The bridge also contributes automatically to IdentityLifecycleService.EraseAsync. Whole-person erasure removes
memberships and tenant-scoped AgentGrant rows for registered tenants, then de-identifies matching deprovisioning
receipts and tenancy audit summaries. The final identity-erasure receipt reports this package as
Koan.Identity.Tenancy; no extra registration is required.
Invitations (PMC-035)
InviteIssuanceService issues a tenant seat to one email address and returns the raw token once — only its
SHA-256 hash is stored. InviteAcceptanceService.AcceptAsync(token, identityId) runs the claim: the signed-in person
must own a verified IdentityEmail matching the invitation, and the claim itself is a conditional write on the
invitation row (Status == Pending && ClaimedBy == null). Two identities racing one token — on one host or across a
fleet — converge to one claimant and one deterministic Membership seat; a claimant whose run was interrupted
re-drives the same token idempotently until they complete or an operator revokes. Issuance, revocation, and
acceptance are audited through the tenant audit log. Reserved host roles (koan:tenancy-operator) can never travel
through an invitation.
var issued = await issuance.IssueAsync(actor, tenantId, "ada@example.com", "editor");
// the invited person, signed in and holding a verified IdentityEmail for that address:
var result = await acceptance.AcceptAsync(issued.Token, identityId);
// result.Outcome: Accepted | AlreadyMember | AlreadyClaimed | NotFound | Expired | Revoked |
// EmailNotOwned | ReservedRoleRefused — reported, never thrown (except the
// conditional-write guarantee: an adapter without write.conditionalReplace refuses at boot of the ceremony).
Over HTTP, Koan.Tenancy.Web exposes the ceremony: POST /api/tenancy/invitations (issue, operator policy),
POST /api/tenancy/invitations/{id}/revoke (operator policy), and POST /api/tenancy/invitations/accept
(any authenticated subject).
Boundaries
- The bridge scopes inbound ASP.NET Core requests. Background work must establish its tenant through the normal captured/explicit Koan context rather than copying a raw header or path value.
- Membership roles cannot project Koan host-operator roles, and invitations cannot grant them either.
- Already-issued bearer tokens remain governed by their issuer outside tenant scope; this package does not revoke OAuth tokens or claim global authorization closure.
- Public/anonymous tenant routing is not a switch on this security boundary and is not currently provided.
- Tenant suspension and custom-domain ownership verification are not enforced here.
- Invitation delivery (email send) is the application's job; this package owns the token, the claim, and the seat.
See TECHNICAL.md and the public tenancy guide.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.JsonPatch (>= 10.0.10)
- Microsoft.AspNetCore.Mvc.NewtonsoftJson (>= 10.0.10)
- Newtonsoft.Json (>= 13.0.4)
- Sylin.Koan.Identity (>= 1.0.61 && < 2.0.0)
- Sylin.Koan.Tenancy (>= 1.0.46 && < 2.0.0)
- Sylin.Koan.Web (>= 1.0.70 && < 2.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Sylin.Koan.Identity.Tenancy:
| Package | Downloads |
|---|---|
|
Sylin.Koan.Tenancy.Web
Reference-mounted tenant registry and membership administration for Koan: a bundled operator UI and API with separated exposure/authority controls, idempotent seat grants, and mutation audit. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.74 | 90 | 9/13/2026 |
| 1.0.70 | 98 | 9/13/2026 |
| 1.0.68 | 90 | 9/13/2026 |
| 1.0.66 | 90 | 9/12/2026 |
| 1.0.62 | 99 | 9/12/2026 |
| 1.0.59 | 111 | 9/10/2026 |
| 1.0.54 | 105 | 9/10/2026 |
| 1.0.49 | 105 | 9/9/2026 |
| 1.0.44 | 102 | 9/9/2026 |
| 1.0.39 | 116 | 9/9/2026 |
| 1.0.37 | 90 | 9/9/2026 |
| 1.0.31 | 103 | 9/9/2026 |
| 1.0.28 | 104 | 9/9/2026 |
| 1.0.22 | 109 | 9/5/2026 |
| 1.0.21 | 119 | 8/30/2026 |
| 1.0.20 | 104 | 8/30/2026 |
| 1.0.19 | 106 | 8/28/2026 |
| 1.0.18 | 105 | 8/28/2026 |
| 1.0.17 | 104 | 8/28/2026 |
| 1.0.16 | 96 | 8/28/2026 |