Release v0.3.0
Added
AcceptInvitationandSetupOrganizationnow reject a second acceptance of the same invitation at append time, via a Chronicle unique-event-type constraint (OneUseJoinTenantInvitation/OneUseCreateTenantInvitation) scoped to the invitation itself. This closes a race where two concurrent submits of the same invitation could both observe it as still pending before either append landed - a rejected second attempt now surfaces as a normal validation failure, never an exception. (#14)- Organization-name uniqueness (
UniqueOrganizationNameConstraint) now covers bothInvitationToCreateTenantAcceptedandOrganizationRegistrationCompletedunder one coordinated constraint, so an invited tenant creation and a self-service registration can no longer claim the same name concurrently. (#14)
Fixed
- The eager organization-name check (
AcceptedOrganizationName) previously only saw names claimed through invited tenant creation. It now also sees names claimed through self-service registration, so the friendly "an organization with this name already exists" message fires for that case too, instead of only surfacing as an append-time rejection.
Summary
Third work package of the onboarding-security epic (#10), building on the verified-owner authorization from #13. It closes the two append-time race gaps this issue calls out: invitation acceptance was only a read-model check, and organization-name uniqueness was enforced for one flow (invited creation) but not the other (self-service registration), so the two could independently let the same name through.
Deferred, with reasons:
- Releasing an organization-name claim when an accepted creation is later revoked/rolled back. There is no host-facing contract event today for reversing an accepted
InvitationToCreateTenantAcceptedorOrganizationRegistrationCompleted- Ante's job ends the moment that event is appended, and revocation in this codebase only ever applies to a still-pending invitation (before acceptance), which never claimed a name in the first place. Wiring aRemovedWith<T>()release path would mean inventing a new cross-repo contract event, which is a product/protocol decision beyond this work package's scope. "Failed acceptance must not strand a claim" is satisfied by construction instead: a rejected command never appends an event, so no claim is ever created for it. - Real concurrent (parallel) append load tests, migration/collision rehearsal tooling, and mixed old/new writer detection from the issue's broader acceptance criteria are infrastructure/ops concerns beyond a single constraint-level change, and aren't exercised by this repo's existing spec conventions (see the pre-existing
UniqueOrganizationNameConstraintspec, which is also sequential, not parallel) - the kernel's append-time constraint check is what actually provides the concurrency guarantee here, independent of how the spec drives it.
Test plan
-
dotnet build -c Debug(solution) - 0 warnings, 0 errors -
dotnet build -c Release -p:CratisProxiesOutputPath=(solution) - 0 warnings, 0 errors -
dotnet test- 92/92 passing, including new specs for: a first acceptance succeeding and a second acceptance of the same invitation being rejected with the specific constraint name (both join-tenant and create-tenant flows), and organization-name uniqueness across invitation/invitation, registration/registration, and mixed invitation/registration collisions, plus a first-claim-succeeds case for each event type