Skip to content

Release v0.3.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 21:20
f1aa2aa

Added

  • AcceptInvitation and SetupOrganization now 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 both InvitationToCreateTenantAccepted and OrganizationRegistrationCompleted under 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 InvitationToCreateTenantAccepted or OrganizationRegistrationCompleted - 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 a RemovedWith<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 UniqueOrganizationNameConstraint spec, 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