Skip to content

fix(auth): make invite token consumption atomic#392

Open
DeryFerd wants to merge 1 commit into
myrialabs:mainfrom
DeryFerd:fix/auth-invite-atomic-consumption
Open

fix(auth): make invite token consumption atomic#392
DeryFerd wants to merge 1 commit into
myrialabs:mainfrom
DeryFerd:fix/auth-invite-atomic-consumption

Conversation

@DeryFerd

@DeryFerd DeryFerd commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR makes invite token usage atomic and concurrency-safe during invite acceptance.

Before this change, invite consumption was implemented as a check-then-act sequence:

  1. Read invite row
  2. Validate expiry / max_uses
  3. Create user + session
  4. Increment use_count

That flow is vulnerable to race conditions under concurrent requests, especially for max_uses = 1 invites. Two requests could both pass validation before either increments use_count, causing oversubscription.

This PR changes the flow so invite usage is consumed under a transaction and guarded update, ensuring one-time invites stay one-time.


Problem

The previous implementation in createUserFromInvite() had a classic TOCTOU pattern:

  • invite.use_count was validated based on a snapshot
  • increment happened later, outside an atomic guard
  • concurrent accepts could both succeed

This means invite links with strict limits (max_uses: 1) were not strongly enforced under contention.


What changed

1) Atomic invite consumption in auth flow

File: backend/auth/auth-service.ts

createUserFromInvite() now:

  • opens a DB transaction with BEGIN IMMEDIATE
  • re-checks invite validity (existence, expiry, usage cap) inside the transaction scope
  • consumes invite usage via guarded query
  • creates user + PAT + session only after successful consume
  • commits on success, rolls back on any failure

This ensures the invite usage decision and user creation happen in one serialized operation.


2) Guarded usage increment query

File: backend/database/queries/auth-queries.ts

incrementUseCount() was changed from:

  • blind increment (UPDATE ... use_count = use_count + 1 WHERE id = ?)

to:

  • conditional increment with guards:
    • respects max-use policy (max_uses = 0 unlimited OR use_count < max_uses)
    • respects expiry (expires_at IS NULL OR expires_at >= now)
  • returns boolean to indicate whether consume succeeded (changes > 0)

This converts usage update from a passive side effect into an explicit success/failure gate used by auth flow.


3) Regression test added

File: backend/database/queries/auth-invite-token.test.ts

New test verifies invite usage cannot exceed cap:

  • create invite with max_uses = 1
  • call incrementUseCount() twice
  • assert use_count remains 1

This test was written as a failing check first, then made green with the guarded update change.


Why this approach

Why BEGIN IMMEDIATE?

Using an immediate transaction acquires a write lock early in SQLite, preventing concurrent writers from interleaving check/consume/create steps. This is a practical and robust choice for serialized invite consumption in this architecture.

Why keep both app-level checks and SQL guards?

  • App-level checks provide clear domain errors and readable flow.
  • SQL guards provide final consistency enforcement at mutation point.
  • Together, they prevent both race-based oversubscription and stale-read edge cases.

Why return boolean from incrementUseCount()?

The caller must know whether usage was actually consumed. Returning boolean allows auth flow to fail fast and map failures to meaningful invite errors.


Behavioral impact

Before

  • Invite validation and usage increment were separated and non-atomic.
  • Concurrent requests could over-consume limited invites.

After

  • Invite usage is consumed transactionally.
  • Limited invites are enforced reliably under concurrency.
  • Expired or exhausted invites fail before account/session creation completes.

Validation

Targeted tests

  • bun test backend/database/queries/auth-invite-token.test.ts
  • bun test backend/ws/auth/login.test.ts

Lint/type checks

  • bun run lint
  • ⚠️ bun run check still reports pre-existing unresolved @myrialabs/ptykit/* module/type issues in terminal-related files (not introduced by this PR).

Risk and compatibility

  • Risk level: Low to Medium
    (Touches auth invite flow and query contract for one method)
  • Backward compatibility: Maintained at API level; behavior change is stricter correctness for invite usage limits.
  • Schema/migration changes: None.

Notes

No changes were made to unrelated auth policies (roles, PAT/session format, invite payload schema).
Scope is intentionally focused on making invite consumption safe and deterministic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant