Skip to content

Commit 914eec5

Browse files
committed
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
1 parent d23d966 commit 914eec5

4 files changed

Lines changed: 56 additions & 28 deletions

File tree

‎.changeset/invitation-invitee-stored-locale.md‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ That reason covers only one of the two populations an invitation reaches. This
1616
change gives both invitation sends the same top rung the other four already
1717
read, on a **two-branch** shape:
1818

19-
1. the address (or phone number) **already carries** a `sys_user` row — an
20-
existing platform user invited into a second organization, a re-invitation,
21-
or an imported phone-only account — the row's `locale` wins;
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;
2222
2. a genuinely **new** invitee with **no** row keeps the deployment default,
23-
because their language is still truly unknown at invitation time.
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.
2426

2527
⛔ The inviter direction stays rejected on both branches, and is now pinned
2628
against a manager that has the top rung wired rather than against one with no
@@ -33,10 +35,20 @@ inviter's header.
3335
address that is already a member of *this* organization
3436
(`USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION`, `routes/crud-invites.mjs` in
3537
the installed 1.7.2), so an existing account invited elsewhere — and the
36-
`resend` branch — reach the callback normally. `sendPhoneInviteSms` is the
37-
branch-1 case in its purest form: its one in-repo caller, the identity import
38-
endpoint's `invite` policy, **creates** the account and only then sends the SMS,
39-
so the row exists before the send does.
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**.
4052

4153
**Matching is exact, and that is safe rather than merely tolerable here.**
4254
better-auth lowercases the invitee address on the invite route and the stored

‎packages/plugins/plugin-auth/src/auth-manager.test.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,14 +2631,21 @@ describe('AuthManager', () => {
26312631
expect(sms.sent[0].body).toContain('验证码');
26322632
expect(sms.sent[0].body).toContain('555555');
26332633
});
2634+
});
26342635

2635-
// ── #14641 — the SMS INVITE path's two branches ───────────────────
2636-
//
2636+
// ── #14641 — the SMS INVITE path gets the same rung ──────────────────
2637+
//
2638+
// Its OWN describe, sibling to #14762 above rather than nested inside it:
2639+
// the reporter path is what the next reader greps, and these pins answer
2640+
// for #14641, not for the card that gave the OTP send its rung.
2641+
describe("#14641 — the invitation SMS reads the invitee's own locale", () => {
26372642
// Was a scope fence ("untouched — its rung is #14641's") until #14641
2638-
// landed. This is the branch-1 case in its purest form: the one in-repo
2639-
// caller (the identity import endpoint's `invite` policy) CREATES the
2640-
// account and only then sends this SMS, so the row exists before the
2641-
// send does — the existing-row branch is not a hypothetical here.
2643+
// landed. The existing-row branch is reachable by construction: the one
2644+
// in-repo caller (the identity import endpoint's `invite` policy) CREATES
2645+
// the account before sending. ⚠️ It does NOT populate `locale`, and the
2646+
// column has no default, so that caller still lands on the deployment
2647+
// rung today — these pins drive the engine directly, which is what lets
2648+
// them measure the rung the in-repo flow does not yet exercise.
26422649
//
26432650
// ⚠️ The two built-in invite bodies genuinely differ, so each assertion
26442651
// names one locale in BOTH directions: the marker for the locale that

‎packages/plugins/plugin-auth/src/auth-manager.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4805,13 +4805,20 @@ export class AuthManager {
48054805
// templates that still interpolate `{{baseUrl}}`.
48064806
//
48074807
// #14641 — the invitee's OWN stored language, on the same two branches the
4808-
// invitation EMAIL takes. This surface is the branch-1 case in its purest
4809-
// form: the doc comment above is not describing a possibility but the
4810-
// invariant of the one in-repo caller — the identity import endpoint
4811-
// CREATES the account and only then sends this SMS, so the row exists
4812-
// before the send does. A number that resolves no row (a caller outside
4813-
// that flow) keeps the deployment default. Matched exactly on
4808+
// invitation EMAIL takes. A number that resolves no row, or a row naming no
4809+
// language, keeps the deployment default. Matched exactly on
48144810
// `phone_number`, the same predicate {@link deliverPhoneOtp} matches on.
4811+
//
4812+
// ⚠️ What this yields TODAY, so the next reader does not over-read it: the
4813+
// one in-repo caller (the identity import endpoint's `invite` policy)
4814+
// CREATES the account before sending, so a ROW is always there — but
4815+
// `admin-import-users.ts` never writes `locale` and the column declares no
4816+
// default, so the value is empty at send time and this path still resolves
4817+
// to the deployment default, exactly as it did before #14641. The rung is
4818+
// wired, not yet exercised in-repo: it answers for an out-of-repo caller,
4819+
// or for a future import that populates the column. ⛔ Do not read this
4820+
// site as a behaviour change users see today — that one is on the
4821+
// invitation EMAIL, whose invitee may already hold a locale-bearing row.
48154822
const storedLocale = await this.storedRecipientLocale({ phone_number: phone });
48164823
const body = await this.renderPhoneSmsBody(
48174824
PHONE_SMS_TOPICS.invite,
@@ -4850,9 +4857,9 @@ export class AuthManager {
48504857
* reads the recipient's own `sys_user.locale` first (#13881, ruling
48514858
* 2026-09-01, the same column the messaging channels resolve per recipient)
48524859
* and falls here when the account holds none. #14641 gave the SMS INVITE
4853-
* path the same rung — matched on `phone_number`, and for the one in-repo
4854-
* caller (identity import) the row always exists, since the account is
4855-
* created before the invitation SMS is sent. This rung answers when the
4860+
* path the same rung, matched on `phone_number` — though the one in-repo
4861+
* caller (identity import) creates a row that never carries a `locale`, so
4862+
* that flow still lands HERE in practice. This rung answers when the
48564863
* number resolves no row, or the row names no language.
48574864
*/
48584865
setDefaultSmsLocale(locale: string | undefined): void {

‎packages/plugins/plugin-auth/src/phone-sms-texts.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
* send-OTP callbacks `{ phoneNumber, code }` and nothing else, so the ruled
2323
* chain (#14788 option D, 2026-09-03) collapses to stored → deployment here.
2424
*
25-
* #14641 gave the SMS **invite** path the same two rungs. Its recipient is
26-
* matched on `phone_number`, and for the one in-repo caller — the identity
27-
* import endpoint's `invite` policy — the account is CREATED before the SMS is
28-
* sent, so the row is there to read. A number that resolves no row keeps the
29-
* deployment default.
25+
* #14641 gave the SMS **invite** path the same two rungs, matched on
26+
* `phone_number`. A number that resolves no row — or a row naming no language
27+
* — keeps the deployment default. ⚠️ That is what the one in-repo caller gets
28+
* today: the identity import endpoint creates the account before sending, so a
29+
* ROW is always there, but it never writes `locale` and the column has no
30+
* default, so that flow still resolves to the deployment rung. The rung is
31+
* wired for an out-of-repo caller, or a future import that populates it.
3032
*
3133
* Whatever arrives, {@link phoneSmsLocaleChain}'s terminal `en` remains the
3234
* floor: this module never returns nothing, and an OTP never fails to render.

0 commit comments

Comments
 (0)