Skip to content

Commit 885451e

Browse files
os-warrenclaude
andcommitted
fix(plugin-auth): say remedy (b) cannot recover an EXISTING person at all
Input from #15587's dev, who searched before concluding it: that fix does not make this remedy true, it makes it fail HONESTLY — the person still cannot register a login, and for an EXISTING person that is by design, not by defect (#14349 is closed on option A, the door stays shut, which the line above already cites). The previous wording rested entirely on forced email verification. That is the story for a NEW address. The operator reading this line is usually trying to recover somebody the directory already holds, and for them the posture is not the lever at all: self-registration is a user-CREATION path, so it cannot hand a login to an address that already carries a `sys_user` row, whatever the posture. Both failures are real; they are different failures, and the more likely one was missing. ⛔ Stated in MECHANISM terms, deliberately. #15587 is a draft under review, so this line asserts no wire status for that refusal — the M2 pin that forbids USER_ALREADY_EXISTS / "answers 200" / "persists nothing" / "no new row" in this message stays green, and the new clause is written to keep it that way. Two more pins: the gate ADMITS this address under `open` with no invitation at all — which is what shows the posture is not what stops an existing person, since whatever refuses them sits downstream of it — and the message states the mechanism rather than a status code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
1 parent db8348e commit 885451e

3 files changed

Lines changed: 42 additions & 8 deletions

File tree

.changeset/boot-sign-in-report-remedy-text.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The `no_sign_in_account_at_boot` report now names a remedy that works — and wa
66

77
That boot line fires on the deployment nobody can sign in to: human `sys_user` rows, zero `sys_account` rows. It ended with two remedies, and measured on the exact population it fires on, neither did what its sentence said:
88

9-
- **"Open the audience posture so an existing person can register their own login"** produced no login. Every posture other than `invite_only` forces `requireEmailVerification` on, so a login registered that way is refused `EMAIL_NOT_VERIFIED` at its first sign-in and a locked-out self-hosted install is usually the shape with no mail transport wired.
9+
- **"Open the audience posture so an existing person can register their own login"** produced no login, and for an existing person it never can: self-registration is a user-creation path, so it cannot attach a login to an address that already carries a `sys_user` row, whatever the posture. Widening only ever admits a *new* address — and then every posture other than `invite_only` forces `requireEmailVerification` on, so that login is refused `EMAIL_NOT_VERIFIED` at its first sign-in, and a locked-out self-hosted install is usually the shape with no mail transport wired.
1010
- **"Write a `sys_account` credential row directly against the store"** was worse than useless. The `password` column carries a secret in the platform's own hash format, so a plaintext one authenticates nothing — and the probe behind this report asks only whether *any* `sys_account` row exists, so writing one turns the report off. The operator's first attempt at the named remedy turned the loud dead end back into the silent one the report was written to end.
1111

1212
The line now names the path that was measured to work: write one pending `sys_invitation` row directly against the store — a lowercase address the directory does not already hold, `status` `pending`, a future `expires_at`, `inviter_id` of any existing `sys_user` — then register through the ordinary sign-up endpoint. The invitation carve-out admits that one creation under every posture, so no door needs widening. It is an admission verdict and not a verification bypass, though, so the line scopes what follows from that: only under the default `invite_only` posture is the recovery mail-transport-free, and it tells the operator to close a widened posture back to `invite_only` before the invited person registers — otherwise the invited login is created, refused `EMAIL_NOT_VERIFIED` at first sign-in, and has silenced this report on the way past. On the `single` tenancy posture that account holder is then promoted to platform admin. The other two are still named, as the two things that look like remedies and are not, because an operator who is going to hand-write a credential row anyway needs to know it blinds the probe.

packages/plugins/plugin-auth/src/boot-sign-in-reachability.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,28 @@ describe('#15588/F1 — the carve-out ADMITS, it does not EXEMPT: the rider\u201
399399
});
400400
});
401401

402+
describe('#15588 — remedy (b) cannot recover an EXISTING person, and the posture is not the lever', () => {
403+
it('under `open` the gate ADMITS the address outright — so the posture is not what stops them', () => {
404+
// The message says widening "cannot recover an EXISTING person at all …
405+
// and NO posture changes that". This is the half that shows WHY the
406+
// posture is not the lever: the audience gate is a CREATION gate and it
407+
// already says yes for this address with no invitation at all. Whatever
408+
// refuses an existing person therefore sits DOWNSTREAM of the posture, so
409+
// moving the posture cannot reach it — which is exactly what the message
410+
// now tells the operator, instead of sending them to widen it.
411+
expect(invitedSignUp('open', false)).toMatchObject({ admit: true });
412+
});
413+
414+
it('the message states it in MECHANISM terms — the operator is told WHY, not a status code', () => {
415+
const msg = resolveNoSignInAccountReport(DEAD_END)!;
416+
expect(msg).toContain('cannot recover an EXISTING person at all');
417+
expect(msg).toContain('user-CREATION path');
418+
expect(msg).toContain('NO posture changes that');
419+
// and the half that IS about a new address survives beside it
420+
expect(msg).toContain('Widening only ever admits a NEW address');
421+
});
422+
});
423+
402424
describe('#15588/N5 — the message NAMES every posture that widens, read off the vocabulary', () => {
403425
it('every self-registration-permitting posture appears in the message by name', () => {
404426
// The message's quantifier ("every posture other than 'invite_only'") is

packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,22 @@
6363
* that would move #14353's diagnostic semantics. The message tells the
6464
* truth about today's probe instead, and the pin fails the day the probe
6565
* changes — which is the day this sentence must be rewritten.
66-
* - **Widening the posture forces email verification on.**
66+
* - **Widening the posture cannot recover an EXISTING person at all, and
67+
* only half-works for a new one.** The operator reading this line is
68+
* usually trying to recover somebody the directory already holds, and for
69+
* them the posture is not the lever: self-registration is a user-CREATION
70+
* path, so it cannot attach a login to an address that already carries a
71+
* `sys_user` row, whatever the posture — the audience gate ADMITS such an
72+
* address under `open` (pinned), which is precisely why moving the posture
73+
* cannot reach what refuses them. ⚠️ Stated in MECHANISM terms on purpose:
74+
* what that refusal answers on the wire is #15587's surface, in flight, and
75+
* this line must stay true on both sides of it — a pin holds it there.
76+
* For a NEW address, widening does create the account, and then
6777
* `audiencePermitsSelfRegistration(posture)` drives `createAuthInstance()`
6878
* to wire `requireEmailVerification: true` (mirrored by
69-
* `getPublicConfig()`), so a login registered under `open`/`email_domain`
70-
* is refused `EMAIL_NOT_VERIFIED` at first sign-in — no login at all on a
71-
* deployment with no mail transport, which is the shape a locked-out
72-
* self-hosted install usually is.
79+
* `getPublicConfig()`), so that login is refused `EMAIL_NOT_VERIFIED` at
80+
* first sign-in — no login at all on a deployment with no mail transport,
81+
* which is the shape a locked-out self-hosted install usually is.
7382
*
7483
* The message deliberately does NOT describe what a SEEDED person's own
7584
* re-registration answers: that response is #15587's surface and is being
@@ -285,8 +294,11 @@ export function resolveNoSignInAccountReport(facts: SignInReachabilityFacts): st
285294
'depending on the row shape, never a session — and ' +
286295
`writing ANY '${SystemObjectName.ACCOUNT}' row SILENCES THIS REPORT, which asks only whether such ` +
287296
'a row EXISTS — the deployment stops being loudly broken and becomes quietly broken; and (b) ' +
288-
"OPENING THE AUDIENCE POSTURE — every posture other than 'invite_only' ('open', 'email_domain') " +
289-
'FORCES email verification ON, so a login registered that way is refused EMAIL_NOT_VERIFIED at ' +
297+
'OPENING THE AUDIENCE POSTURE — which cannot recover an EXISTING person at all: ' +
298+
'self-registration is a user-CREATION path, so it cannot hand a login to somebody whose ' +
299+
`'${SystemObjectName.USER}' row already exists, and NO posture changes that. Widening only ever ` +
300+
"admits a NEW address — and then every posture other than 'invite_only' ('open', " +
301+
"'email_domain') FORCES email verification ON, so that login is refused EMAIL_NOT_VERIFIED at " +
290302
'its first sign-in until a mail transport delivers the link, and a locked-out self-hosted ' +
291303
'install usually has none. Nothing here happens by itself.'
292304
);

0 commit comments

Comments
 (0)