Skip to content

fix: unbreak the invitation flow and email-shaped-username login - #6927

Merged
otavio merged 3 commits into
masterfrom
fix/invite-translation-and-login-identifier
Aug 19, 2026
Merged

fix: unbreak the invitation flow and email-shaped-username login#6927
otavio merged 3 commits into
masterfrom
fix/invite-translation-and-login-identifier

Conversation

@otavio

@otavio otavio commented Aug 19, 2026

Copy link
Copy Markdown
Member

What

Fixes the three defects behind a customer being unable to accept a workspace invitation, and unblocks the 149 production accounts whose username is an email address.

Why

The customer's browser showed the top-level ErrorBoundary on /accept-invite:

Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.

The backend was healthy — the user was confirmed, the invitation pending and unexpired, the cache entry present. The failure was in the browser, and separately in login identifier resolution.

Changes

  • ui, translation opt-out: translate="no" plus <meta name="google" content="notranslate"> on the console, its migrate entry, and the website. Chrome's auto-translation moves each text node into a <font> wrapper; the node survives, so React keeps writing to it, but it is no longer a child of the parent React recorded. The next insertBefore or removeChild throws NotFoundError. Nothing in ui/ used the HTML translate attribute, so the opt-out breaks no intentional per-element usage.

  • ui, bare text nodes: wrapped the bare text in the AcceptInvite and Login alert subtrees in <span>. React materialises a text node as a fiber only when text and elements are siblings; such a node is fatal under a translated DOM whenever React must insert a sibling before it or remove it. An element anchor survives translation.

  • server, identifier resolution: AuthLocalUser now tries the email column first and falls back to the username column. The username rule in pkg/validator has always permitted @ (^([a-z0-9-_.@]){3,32}$), so an email-shaped identifier is not necessarily an email. Resolving only by email returned a permanent 401. The rule is extracted to store.UserResolveByAuthIdentifier because cloud repeats it in three recovery flows.

  • website: added the missing src/vite-env.d.ts so tsc -b resolves the ?raw import the new test needs.

Testing

simulateBrowserTranslation() (src/tests/simulateBrowserTranslation.ts) reproduces Chrome's rewrite in jsdom. The AcceptInvite test fails on the unpatched page with NotFoundError: The node to be removed is not a child of this node.

Worth knowing while reviewing:

  • Login.tsx never crashed. Its conditional is the last child, so React appends, which is safe. It is hardened, not fixed — no test can go red for it.
  • 52 sites elsewhere still carry the shape (22 insert-before, 30 remove-child), across billing, dialogs, install-keys, sessions and the website. translate="no" already protects all of them in Chrome. They remain exposed to anything else that rewrites the DOM. Not fixed here to keep the blast radius small.
  • Auto-translation was not verified in a real Chrome — this environment has no browser with a non-English UI language. Only the DOM rewrite and the resulting React error class were reproduced. Please confirm in a real Chrome before release.

The store integration tests (api/store/pg, TestEnrollmentE2E_*) need testcontainers and were not run.

@otavio
otavio requested review from a team as code owners August 19, 2026 19:17
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Code Review Complete

The automated review ran but did not post an updated summary — this usually means no new issues were found since the previous review. If you've pushed changes and want a fresh pass, comment /review.

View job

otavio added 3 commits August 19, 2026 17:19
Chrome's auto-translation moves each text node into a <font> wrapper. The text node
survives, so React keeps writing to it, but it is no longer a child of the element React
recorded as its parent, and the next insertBefore or removeChild against that parent throws
NotFoundError. The top-level ErrorBoundary then replaces the page.

A customer hit this on /accept-invite and could not accept her workspace invitation.

The website app gains vite-env.d.ts because it was missing the standard Vite type
reference, so tsc could not resolve the ?raw import the new test needs.
React materialises a text node as a fiber only when text and elements are siblings. Such a
node is fatal under a translated DOM whenever React must insert a sibling before it or
remove it, because translation has moved it inside a <font> wrapper.

On AcceptInvite this crashed for real: advancing from the sign-up branch to the joined
branch removes the description's bare text nodes. Login's countdown only ever appends, so
it never crashed, but the shape is one reorder away from doing so.

Wrapping each bare text in a span gives React an element anchor, which translation leaves
in place. The translate="no" opt-out already covers Chrome; this covers anything else that
rewrites the DOM, such as an extension or an in-page translator.
Usernames may legally contain "@" -- the username rule in pkg/validator has always allowed
it -- so an email-shaped identifier is not necessarily an email. Resolving such an
identifier only by email locked those accounts out of their own username behind a permanent
401. In production 149 of 6914 users have "@" in the username.

The lookup now tries the email column first, which keeps the common case unchanged, then
falls back to the username column. Lockout accounting is untouched: it keys on user.ID and
runs after resolution. The extra query happens only on the path that already returned 401
immediately, so it opens no new timing channel.

The rule lives in store.UserResolveByAuthIdentifier because cloud repeats it in three more
flows.
@otavio
otavio force-pushed the fix/invite-translation-and-login-identifier branch from c38f23c to a725866 Compare August 19, 2026 20:26
@otavio
otavio enabled auto-merge (rebase) August 19, 2026 20:27
@otavio
otavio disabled auto-merge August 19, 2026 20:27
@otavio
otavio merged commit b9a09ca into master Aug 19, 2026
34 checks passed
@otavio
otavio deleted the fix/invite-translation-and-login-identifier branch August 19, 2026 20:27
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