Accept invitations into a cross-product organisation, and stop the DB test flake - #44
Merged
Merged
Conversation
…accepted `acceptTeamInviteWithinTransaction` states its own invariant plainly: one organisation is usable from every UOA-integrated product, so the org's origin domain is not an acceptance predicate. The scope gate at the end of the same function then contradicted it, filtering the invitee's memberships by `org.domain = config.domain`. A cross-product invitee can never satisfy that: they hold no membership row anywhere until this call creates one, and the org belongs to whichever product founded it. Acceptance was refused with a bare 401, surfacing as "Invitation unavailable" on the mail-bound flow. This is the live failure behind the invitations that never landed: the organisation was founded through one product and invited into through another. Both gates now use the client/product-aware variants that already exist for exactly this. The tombstone rule is untouched — exact ACTIVE organisation and team membership is still required — and the domain relaxation applies only where the inviting domain resolves to a single active product service. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`clientDomain.create` takes `label`, not a client secret hash. CI caught it; Docker had died locally before the test could run once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`billing_app_keys_purpose_origins_check` requires them for CUSTOMER_LIFECYCLE; dropping the field from the copied fixture broke the insert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-product membership and the single-product policy are read on the BYPASSRLS client, which is resolved once from the ambient environment and so never sees a test's isolated schema. Acceptance now takes them as optional dependencies; production callers pass nothing and behave exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three fixes, found while verifying a live invitation end to end against production.
team-invite.service.acceptance.ts) — the function states that an organisation is usable from every UOA product, then filtered the invitee's memberships by the inviting product's domain. A cross-product invitee holds no membership row until acceptance creates one, so every such acceptance was refused with a bare 401 and rendered as "Invitation unavailable". Both gates now use the existing client/product-aware variants; the ACTIVE-membership tombstone rule is unchanged and the relaxation is gated on the inviting domain resolving to a single active product service. This is the security-relevant change in this PR and deserves the closest look.email-registration-link.ts) — consuming an invitation writes a domain role and two memberships, which row-level security refuses to the app role. The path added for existing accounts ran on the wrong connection and returned a 500 in production. Its regression test moved from the superuser-backedcreateTestDbtocreateRlsTestDb, and now reproduces that 500 exactly when the connection is wrong.vitest.config.ts+ 14 test files) — fourteen provisioningbeforeAllhooks passed their own timeout, overriding the configured 180s. The two set to 30s could not finish:createTestDbbacks off ~31s across six startup retries, so the hook was killed mid-retry. That tookAPI tests (Postgres)red at random. A unit test now names any hook that reintroduces one.Testing
pnpm --filter @uoa/api testand the full Postgres integration suite pass locally, including the RLS-backed invitation regression.🤖 Generated with Claude Code