You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(auth): an invitation reads the invitee's own sys_user.locale when the address already holds a row (#15119)
* fix(auth): an invitation reads the invitee's own `sys_user.locale` when the address already holds a row
The two invitation sends kept the deployment rung while the other four auth
sends gained a per-recipient one in #14762. The recorded reason — an invitee
has no `sys_user` row until acceptance — covers only one of the two
populations an invitation reaches, so both sends now take a two-branch shape:
1. an address / phone number that ALREADY carries a row (an existing user
invited into a second organization, a re-invitation, or an imported
phone-only account) uses that row's `locale`;
2. a genuinely new invitee with no row keeps the deployment default.
The inviter direction stays rejected on both branches, now pinned against a
manager that HAS the top rung wired rather than one with no rung at all.
Reachability of branch 1 is measured, not assumed: better-auth's
`create-invitation` rejects only an address already a member of THIS org
(`routes/crud-invites.mjs`, installed 1.7.2), and the SMS invite's one in-repo
caller creates the account before it sends.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
* docs(auth): state what the SMS invite rung yields today, and give its pins their own #14641 describe
Two wording items from the Clause-2 contract review, which passed. No
behaviour, assertion or fixture changes.
1. The changeset claimed an imported phone-only account's `locale` wins on the
SMS invite path. Re-measured: `admin-import-users.ts` contains 0
occurrences of `locale` (positive control: `sendInviteSms` twice in the
same file) and `sys_user.locale` declares no column default (positive
control: `defaultValue` appears 15 times elsewhere in that object). So on
the only in-repo caller the row exists but its column is empty at send
time, and that flow still resolves to the deployment default — the
pre-change behaviour. The rung is wired and answers for an out-of-repo
caller or a future import that populates the column; a changeset becomes
release notes, so it now says exactly that. The same overstatement is
corrected in the three code comments carrying it (`sendPhoneInviteSms`,
`setDefaultSmsLocale`, `phone-sms-texts.ts`).
2. The five SMS invitation pins were nested inside `describe('#14762 ...')`,
so their reporter path credited the wrong card. They now sit in their own
sibling describe naming #14641. Move only — no re-indentation, and no
`expect`/`await`/`const` line in the diff.
`content/docs/**` deliberately untouched: the review found the shipped
sentence literally accurate as written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(auth): an invitation is written in the invitee's own `sys_user.locale` when the address already holds a row, and keeps the deployment default when it does not (#14641)
6
+
7
+
The four auth sends whose requester IS the recipient gained a per-recipient
8
+
language rung in #14762 (`sys_user.locale`, ruled on #13881). The two
9
+
**invitation** sends did not, and the recorded reason was structural rather
10
+
than an oversight: an invitee generally has no `sys_user` row until they accept,
11
+
so there is no stored language to read, and the *inviter's*`Accept-Language` is
12
+
the wrong authority — an English-speaking admin would silently send English
13
+
invitations to a Chinese-language workspace's new hires.
14
+
15
+
That reason covers only one of the two populations an invitation reaches. This
16
+
change gives both invitation sends the same top rung the other four already
17
+
read, on a **two-branch** shape:
18
+
19
+
1. the address (or phone number) **already carries** a `sys_user` row whose
20
+
`locale` is set — an existing platform user invited into a second
21
+
organization, or a re-invitation — that row's `locale` wins;
22
+
2. a genuinely **new** invitee with **no** row keeps the deployment default,
23
+
because their language is still truly unknown at invitation time. So does an
24
+
invitee whose row exists but names no language: an unset column is not a
25
+
choice.
26
+
27
+
⛔ The inviter direction stays rejected on both branches, and is now pinned
28
+
against a manager that has the top rung wired rather than against one with no
29
+
rung at all. #13881's ruling item 3 fixes the chain as **recipient** locale →
30
+
deployment default; what opened here is the invitee's own column, never the
31
+
inviter's header.
32
+
33
+
**Both branches are reachable, measured rather than assumed.**
34
+
`sendInvitationEmail`: better-auth's `create-invitation` route rejects only an
35
+
address that is already a member of *this* organization
36
+
(`USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION`, `routes/crud-invites.mjs` in
37
+
the installed 1.7.2), so an existing account invited elsewhere — and the
38
+
`resend` branch — reach the callback normally. `sendPhoneInviteSms` reaches a row by
39
+
construction: its one in-repo caller, the identity import endpoint's `invite`
40
+
policy, **creates** the account and only then sends the SMS.
41
+
42
+
⚠️ **What the SMS path yields today, stated precisely, because a changeset
43
+
becomes release notes.** The rung is wired there and reads the row whenever the
44
+
row carries a locale — but `admin-import-users.ts` never writes `locale` (0
45
+
occurrences; positive control: `sendInviteSms` appears twice in the same file),
46
+
and `sys_user.locale` declares no column default. So on the only in-repo caller
47
+
the column is empty at send time and the invitation SMS still resolves to the
48
+
**deployment default** — the pre-change behaviour, unchanged for that flow. What
49
+
this buys on that surface is the rung itself: an out-of-repo caller, or a future
50
+
import that populates `locale`, is read rather than ignored. The behaviour users
51
+
see change today is on the invitation **email**.
52
+
53
+
**Matching is exact, and that is safe rather than merely tolerable here.**
54
+
better-auth lowercases the invitee address on the invite route and the stored
55
+
`user.email` on sign-up, so both sides of the predicate are already in the same
56
+
case; `email` and `phone_number` are both `unique: true` in the `user` table
57
+
`sys_user` is backed by. An address that resolves no row lands on the deployment
58
+
default, which is the documented floor rather than a failure — and, as
59
+
everywhere else on this ladder, a failing recipient read never blocks a send.
60
+
61
+
**Docs.**`permissions/authentication.mdx` said "The **invitation** SMS reads
62
+
the deployment default alone"; that sentence is now false and is corrected. No
63
+
shipped page states the invitation *email* locale rule (the auth email ladder is
0 commit comments