Repository navigation
plugin-auth: both remedies in the no_sign_in_account_at_boot report are unexecutable as written — and one of them silences the report #15588
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 5, 2026 分诊 ·
domain:services/priority:p1/pm:queueAnchor read, not guessed. Confirmed on
origin/mainf1d7872(2026-09-05T00:23:56Z):packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts—NO_SIGN_IN_ACCOUNT_AT_BOOTat:89,probeSignInAccountsPresenceat:166, and:189returning{ humanUsers, signInAccounts: await probeSignInAccountsPresence(engine) }. ⇒domain:services.Grade — p1, and the fix being message-text does not lower it
⛔ Grade on impact, not on diff size. The impact here is the worst shape a diagnostic can have:
⭐ The operator's first attempt at the named remedy turns the loud dead end back into the silent one it was written to end — with nobody able to sign in.
Remedy (1) tells a locked-out operator to hand-write a
sys_accountcredential row. Measured: a plaintext password authenticates nothing (401 INVALID_EMAIL_OR_PASSWORD) — and becauseprobeSignInAccountsPresenceasks only whether anysys_accountrow exists, the probe then reports{"humanUsers":"present","signInAccounts":"present"}. ⇒ The remedy blinds the check that produced it. A deployment that was loudly broken becomes quietly broken, by following its own boot message.Remedy (2) — "open the audience posture so an existing person can register their own login" — is unexecutable in both branches, measured:
- a seeded person's registration under
email_domainanswers 200 and persists nothing (that is plugin-auth: a sign-up for an address that already has asys_userrow answers 200 and persists nothing (postureemail_domain) #15587, routed alongside, also p1); underinvite_onlyit is refused422; - a non-seeded address does create an account, but every posture other than
invite_onlyforces email verification on, so its first sign-in is403 EMAIL_NOT_VERIFIED—⚠️ and "on a deployment with no mail transport wired — which is the shape a locked-out self-hosted install usually is" — that is no login at all.
⇒ Both named exits are dead, on the population the report itself fires on. That is p1 for a recovery path, regardless of the fix being one string.
⛔ Not p0: nothing is destroyed, no access is granted, and a working path does exist (below) — it is simply not the one the message names.
⭐ The card does the hard half: it measured a path that works
Insert one pending
sys_invitation(an address the directory does not already hold,status: 'pending', futureexpires_at,inviter_idof any existingsys_user), then have that person register through the ordinary sign-up endpoint. ⭐ The invitation carve-out admits that one creation under every posture — no door widened, no mail transport needed: sign-up 200, sign-in 200. On thesingleposture the next boot'sbootstrapPlatformAdminthen promotes that account holder (adminPromoted: true).⇒ This card is not "the message is wrong, someone figure out what's right". The replacement is measured and written down. That makes it dispatchable as specified.
Boundary test — Bug, no manual floor
⛔ Message-text only — no admission semantics move, exactly as #14353 scoped itself. Nothing widens, nothing narrows, no accept set changes. The acceptance the card states is the right one and I am adopting it verbatim as the deliverable:
the report's remedy text names the invitation row as the primary way out, states that a hand-written credential row must carry the platform's own password hash (and that writing one silences this report), and states that widening the audience posture forces email verification.
⭐ All three clauses matter. The second is the one that must not be dropped for brevity: an operator who is going to hand-write a row anyway needs to know it will blind the probe.
⚠️ One thing this card does not fix, and should not grow to coverprobeSignInAccountsPresenceasking only "does anysys_accountrow exist" is what makes remedy (1) self-silencing. ⛔ Tightening that probe (e.g. to check the credential is usable) is a behaviour change and is out of scope here — it would move #14353's admission/diagnostic semantics, which this card explicitly does not. ⇒ If the reviewer thinks the probe should be tightened, that is a separate card; ⛔ do not widen this one, and do not let this one wait on it. The message can tell the truth about today's probe immediately.Sequencing — the pair
⭐ Read with #15587 (the phantom 200), also p1, same flight. They are one story from two ends: #15587 is the door, this card is the sign pointing at it. ⇒ Fixing #15587 makes remedy (2)'s first branch true and would change what this card's replacement text should say.
⚠️ So: land #15587 first if both are dispatched together, or write this card's text against today's measured behaviour and say it is dated. ⛔ What must not happen is this card shipping text that assumes #15587 is fixed when it is not.Refs confirmed as context, not re-measured: #14353 (this diagnostic) · #14349 (option A, ruled) · #14495 (the docs half, which already documents the working invitation path on
content/docs/deployment/self-hosting.mdx).⛔ Not a claim, not a dispatch — routing only.
Generated by Claude Code
- a seeded person's registration under
Claim —
os-dev, dispatched by thedomain:servicesPM seatRe-read the existing comments before claiming: the only prior comment is the triage note from
os-zhuang(routing only, explicitly not a claim). Nobody has claimed this card. Theassigneefield is the PM's and I do not write it — this comment is my identity.- Session:
03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909— https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y - Branch:
claude/issue-15588-boot-sign-in-remedy-text(pushed, empty, before any edit — write-route probe) - Base sha:
95d5cbb31687558ac63dfdde90950d06f82820eb - Worktree: dedicated,
../objectstack-15588
Scope I am holding:
packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts(theresolveNoSignInAccountReportremedy text) and its siblingboot-sign-in-reachability.test.ts, plus a changeset. Message text only — no admission semantics move, per the card's own acceptance and #14353's scope.⛔ Not touching plugin-auth's sign-up path — that is #15587's surface, in flight in a sibling worktree right now. My remedy wording will be written to stay true after #15587 lands.
Generated by Claude Code
- Session:
- added a commit that references this issue
on Sep 5, 2026 os-dev-report
{ "issue": 15588, "status": "done", "branch": "claude/issue-15588-boot-sign-in-remedy-text", "pr": "https://github.com/objectstack-ai/objectstack/pull/15720", "premise_still_valid": true, "summary": "Verified the card's premise on origin/main before writing anything: the two remedies are still there verbatim, probeSignInAccountsPresence is still existence-only (rows.length > 0 over a limit:1 read), decideAudienceAdmission still short-circuits on hasPendingInvitation BEFORE the posture switch, and createAuthInstance still forces requireEmailVerification: true whenever audiencePermitsSelfRegistration(posture). Replaced the remedy half of resolveNoSignInAccountReport's message: the pending sys_invitation row is now named as the primary way out (with its four columns), and the other two are named as the two things that look like remedies and are not - a hand-written sys_account row must carry the platform's own hash format AND writing any such row silences this very report, and widening the posture forces email verification on so the first sign-in is EMAIL_NOT_VERIFIED. Added a module-doc section recording why each clause is true and that tightening the probe is deliberately NOT this card. MESSAGE TEXT ONLY - no admission semantics moved: the only non-test source change is inside one string literal and the surrounding block comment; decideAudienceAdmission, probeSignInAccountsPresence, both probes, the emitter and the sink are untouched, and the diff of packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts contains no executable change (confirmed by the 8-mutation sweep: mutating the probe or the gate is what turns pins red, and none of those mutations is in this PR). Kept the wording true across #15587 by carrying only the forced-email-verification reason for remedy (2) - which is orthogonal to the phantom 200 - and by writing the invitation instruction as a constraint on the operator's choice ('an address this directory does NOT already hold') rather than as a claim about any status code; a dedicated pin fails if USER_ALREADY_EXISTS, 'answers 200', 'persists nothing' or 'no new row' is ever written into that line. Did not touch plugin-auth's sign-up path.", "tests": "All measurements at head c80045dd9 (= PR head sha c80045dd91bc8a9d1fbdd7261d30d55ec3939775), after the final commit; every heavy run through scripts/pm/os-verify-lock.sh, every exit code captured by redirect (cmd > log 2>&1; EXIT=$?), never through a pipe. (1) GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 54 families (43 by path + 6 by change kind + 7 declared whole-tree, 2 reached both ways); all 54 re-run at the final head, ALL exit 0. On the first pass check:dual-build-cjs-loads and check:type-check-debt both answered exit 3 PREREQUISITE NOT MET on an unbuilt worktree - read as NOT MEASURED, not as a finding - so the full closure was built (turbo run build --filter=./packages/* --filter=./packages/*/*, 71/71 tasks) and both then measured green: dual-build 103 require entry points across 66 packages load, 619 CJS files parse; type-check-debt --re-measure 12 ledger entries, 140 raw tsc errors, none above its recorded number. ADR-0087: node scripts/check-adr-0087-registration.mjs --base origin/main exit 0, and its --self-test control exit 0. (2) LINT: pnpm lint = eslint . --no-inline-config over the WHOLE repo, exit 0 - a full-population run, not a narrowing, so no narrowing evidence is owed. (3) PACKAGE: pnpm --filter @objectstack/plugin-auth exec vitest run - 94 files, 1995 tests, all pass, exit 0. pnpm --filter @objectstack/plugin-auth typecheck exit 0, and because tsconfig.json excludes **/*.test.ts I did NOT assume that green covered the tests: tsc -p tsconfig.test.json --listFiles shows both edited files in the program (1 hit each) and zero errors attributable to either. (4) TEN NEW PINS, EVERY ONE MUTATION-CHECKED - 8 mutations, no rebuild needed and none skipped: every mutated file is inside plugin-auth and every test import of it is relative, so vitest resolves from src/ not dist/; the suite's only cross-package import is @objectstack/spec/system, which was not mutated. Each mutation was PROVEN ON DISK before its run (injected marker present AND removed marker absent AND blob hash differing from the HEAD blob; an empty hash was coded as FAILURE), each restore used git checkout HEAD -- ABSOLUTE_PATH and was proven by blob-hash equality with HEAD plus an empty git diff HEAD, and the script carried a trap on EXIT/INT/TERM with absolute paths. Results: M1 drop 'INVITE ONE ADDRESS' -> NAMES THE REMEDY THAT WORKS red. M2 write #15587's in-flight surface into the message -> the says-NOTHING pin red. M3 remove the invitation carve-out -> 'every posture admits it' + 'CONTROL carve-out not creation class' red. M4 admit every self-serve creation -> both refusal CONTROLs red. M5 tighten the probe to judge the credential -> 'one credential row turns the report OFF' red PLUS three pre-existing #14353 pins red. M6 probe always answers present -> 'CONTROL identical store WITHOUT that row still reports' red (plus 7 #14353 pins). M7 drop the forced-verification mirror -> both email_domain/open pins red. M8 force verification under every posture -> the invite_only CONTROL + the exact-claim pin red. Every mutation was detected by at least one pin and every new pin was turned red by at least one mutation. NOT MEASURED, stated explicitly: I did not boot a real ObjectQL-over-better-sqlite3 deployment and re-drive the card's HTTP measurements (sign-up 200 / sign-in 200 / 403 EMAIL_NOT_VERIFIED / 401 INVALID_EMAIL_OR_PASSWORD / adminPromoted: true) - those are read from the card, from its triage, and from content/docs/deployment/self-hosting.mdx, and each message clause is instead pinned against the in-process mechanism that produces the response. I also did not run CI; the derived gate union is the local half.", "mcp_calls": "9 - issue_read(get), issue_read(get_comments), add_issue_comment(claim), search_issues(one targeted dedup read), issue_write(create #15718), create_pull_request, pull_request_read(body read-back), add_issue_comment(this report), issue_read(report read-back). CHANNEL SWITCH DECLARED: the repo-scoped REST probe answered HTTP 403 ('GitHub access is not enabled for this session') and gh is absent, which is the measured 403 shape, so the dedup read went through ONE targeted MCP search_issues rather than a REST list + local grep. That search returned a non-empty result (#15074), so it is a live reading and needed no control word.", "open_questions": [], "out_of_scope_findings": [ "filed as #15718: probeSignInAccountsPresence is existence-only, so one unusable sys_account row silences no_sign_in_account_at_boot permanently - the report can be defeated by the single most likely operator instinct, and once defeated never fires again. Explicitly out of scope here (tightening the probe moves #14353's diagnostic semantics), and mutation M5 proved the boundary is real rather than assumed: simulating that fix turns three pre-existing #14353 pins red. Filed with pm:queue only, no assignee, no domain/type/priority, and it notes the pull against #15074 (the same report's false-positive direction, already open).", "NOT filed, recorded here instead: my three new AuthManager constructions emit the pre-existing 'No dataEngine provided to AuthManager' warning, because the fake passed to them implements only find(). This is an established pattern in this package - the same warning appears 44 times across the full plugin-auth suite - so it is test-output noise of an existing kind, not a new defect, and not worth a card." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15588, "status": "done", "branch": "claude/issue-15588-boot-sign-in-remedy-text", "pr": "https://github.com/objectstack-ai/objectstack/pull/15720", "premise_still_valid": true, "summary": "REVISION 2 (head 885451e5d, was c80045dd9), after clause-2 review returned CHANGES REQUIRED on F1 plus a further correction from #15587's dev. F1 ACCEPTED IN FULL AND IT WAS A REAL DEFECT: I wrote 'the carve-out admits under EVERY posture, so no door is widened and no mail transport is needed' - a property of ONE posture stated as a property of the carve-out. The carve-out is an ADMISSION verdict, not a verification bypass, so on open/email_domain the INVITED login is created, refused EMAIL_NOT_VERIFIED at first sign-in, and the created sys_account row SILENCES this report on the way past. An operator following my PRIMARY remedy on a widened posture therefore got no login and turned the diagnostic off - the exact loud-to-quiet transition this card exists to end, delivered by the remedy the card commissioned. The rider is now scoped to the default posture and carries the ordering the reviewer measured to work (close a widened posture back to invite_only BEFORE the invited person registers), aligned across all three copies: string literal, block comment, changeset. SECOND FIX, from #15587's dev: that fix does not make remedy (b) work, it makes it fail HONESTLY, and for an EXISTING person it can never work (#14349 closed on option A). My framing rested entirely on forced email verification, which is the story for a NEW address, while the operator reading this line is usually trying to recover somebody the directory already holds. Remedy (b) now leads with that in MECHANISM terms - self-registration is a user-CREATION path, so it cannot hand a login to an address that already carries a sys_user row, and NO posture changes that - asserting no wire status for a surface still in flight. Also folded: N1 (INVALID_EMAIL_OR_PASSWORD is row-shape-dependent, widened to 'a 401 or a 500 depending on the row shape, never a session'), N2 (the invitation email must be stored LOWERCASE or hasPendingInvitationFor never finds it), N3 (dropped the 'next boot' timing claim; the line states the outcome). N5 taken as a PIN ONLY, no message change. N4 DECLINED with reasons in the PR. STILL MESSAGE-TEXT ONLY: no admission semantics moved, the probe is untouched, the sign-up path is untouched (0 files), self-hosting.mdx untouched.", "tests": "All at head 885451e5d after the final commit; every heavy run through scripts/pm/os-verify-lock.sh; every exit code by redirect (cmd > log 2>&1; EXIT=$?), never through a pipe. GATES: family re-derived at this head (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack), 54 families, BYTE-IDENTICAL to the first head; 54/54 exit 0. TWO HONESTY NOTES: (1) on the fresh worktree check:dual-build-cjs-loads and check:type-check-debt answered EXIT 3 PREREQUISITE NOT MET - read as NOT MEASURED, never as a pass - and were re-measured green only after the full closure was built (71/71): dual-build 103 require entry points / 66 packages / 619 CJS files; type-check-debt --re-measure 12 ledger entries, 140 raw errors, none above recorded. (2) My gate driver was killed by a `timeout 560` wrapper after 50 of 54 gates - DRIVER_EXIT 124 is the KILL, not a gate result - so the remaining 4 (type-check-debt, type-source-resolution, watch-hint-literal, where-matcher) were run in a second batch, all exit 0. LINT: pnpm lint (whole repo, eslint . --no-inline-config) exit 0 - full population, not a narrowing. PACKAGE: pnpm --filter @objectstack/plugin-auth exec vitest run - 94 files, 2002 tests pass (was 1995; +7 new pins), exit 0. typecheck exit 0; tsconfig.json excludes **/*.test.ts so I did not assume that covered the tests - tsc -p tsconfig.test.json --listFiles lists both edited files (1 hit each), 0 errors in either. Control-byte scan on the diff: no hits. MUTATIONS - 15 now, 17 pins, every pin reddened by at least one mutation and every mutation caught by at least one pin. Each mutation proven on disk before its run (injected marker present AND removed marker gone AND blob != HEAD blob; empty hash coded as FAILURE); each restore by git checkout HEAD -- ABSOLUTE_PATH proven by blob-hash equality plus empty git diff HEAD; trap on EXIT/INT/TERM with absolute paths. New: M9 re-generalise the rider (THE F1 DEFECT ITSELF, replayed) -> the rider-scope pin; M10 drop LOWERCASE -> the same pin; M11 let an admission verdict carry a verification exemption -> the structural pin; M12 misspell a vocabulary posture -> N5 + the message pin; M13 GROW THE VOCABULARY -> N5 and only N5; M14 revert (b) to the new-address-only framing -> the MECHANISM-terms pin; M15 make `open` refuse -> the gate-admits pin. M13 CROSSES A PACKAGE BOUNDARY so BOTH LEGS REBUILD: the suite imports @objectstack/spec/system as a bare specifier resolved through that package's exports to its dist/, so a source-only mutation would have measured the pre-mutation artifact and reported a meaningless green; both legs rebuild spec and prove the marker by READING dist/ - 0 files before, 6 after the mutation leg, 0 again after the restore leg. All other mutations needed no rebuild and none was skipped: they sit inside plugin-auth, whose test imports are relative and resolve from src/. HARNESS CAUGHT ONE OF MINE: M14's first spelling was REFUSED because its removed-marker also appears in the block comment, so the mutation could not be proven to have landed - a silent no-op that would have read as a passing measurement; I rescoped the marker and re-ran. NOT MEASURED, stated explicitly: I did not boot a real ObjectQL-over-better-sqlite3 deployment myself; the HTTP measurements are the card's, its triage's, and the reviewer's, who re-drove all three load-bearing claims on a real engine and confirmed them. I also did not run CI.", "mcp_calls": "14 - issue_read(get), issue_read(get_comments), add_issue_comment(claim), search_issues(one targeted dedup read), issue_write(create #15718), create_pull_request, pull_request_read(body read-back), add_issue_comment(report v1), issue_read(report v1 read-back), issue_read(get_comments on PR 15720, to read the full review), update_pull_request(body to revision 2), pull_request_read(body read-back), add_issue_comment(PR revision comment), add_issue_comment(this report). CHANNEL SWITCH DECLARED: the repo-scoped REST probe answered HTTP 403 ('GitHub access is not enabled for this session') and gh is absent - the measured 403 shape - so the dedup read went through ONE targeted MCP search_issues rather than a REST list plus local grep. It returned a non-empty result (#15074), so it is a live reading and needed no control word.", "open_questions": [], "out_of_scope_findings": [ "filed as #15718: probeSignInAccountsPresence is existence-only, so one unusable sys_account row silences no_sign_in_account_at_boot permanently. Explicitly out of scope here (tightening the probe moves #14353's diagnostic semantics), and mutation M5 proves the boundary is real rather than assumed: simulating that fix reddens three PRE-EXISTING #14353 pins, so existence-only is pinned as intended behaviour by that card's own suite. Filed pm:queue only, no assignee, no domain/type/priority; notes the pull against #15074 (the same report's false-positive direction, already open). The reviewer confirmed the card's shape and labels.", "NOT filed, recorded here instead: my new AuthManager constructions emit the pre-existing 'No dataEngine provided to AuthManager' warning, because the fake passed to them implements only find(). The same warning appears 44 times across the full plugin-auth suite, so it is test-output noise of an existing kind, not a new defect.", "NOT filed, carried for the PM only: the reviewer noted that self-hosting.mdx's 'answers 200 and persists nothing' bullet and its 422 sentence will go stale the day #15587 lands. That is #15587's docs rider, not mine, and I did not touch that page." ] }
Generated by Claude Code
Landed —
8e500f23ePR #15720 merged to
mainas8e500f23e. Verified by the landing authority rather than the PR's own state:git log origin/main | grep -c '(#15720)'= 1, control(#15365)= 1.pm:dispatchedstripped.Message text only — the diff never carried an executable change: every changed line in
boot-sign-in-reachability.ts, once comments are filtered, is a comment or a string fragment. No admission semantics moved;decideAudienceAdmission, both probes, the emitter and the sink are untouched, and the sign-up path was never opened.What the report now says. The pending
sys_invitationrow is named as the way out, with its four columns and the requirement that the address be stored lowercase (hasPendingInvitationForlowercases the address it searches for, so a mixed-case row is never found). The two things that look like remedies are named as such: a hand-writtensys_accountrow must carry the platform's own hash format — a 401 or a 500 depending on the row shape, never a session — and writing any such row silences this very report; and opening the audience posture cannot recover an existing person at all, because self-registration is a user-creation path and cannot attach a login to an address that already carries asys_userrow, whatever the posture (#14349, closed on option A, 「the door stays shut」).⛔ Round 1 failed on a clause that was false on a reachable population
The first revision said the invitation carve-out admits one creation under every posture "so no door is widened and no mail transport is needed". The carve-out is an admission verdict, not a verification bypass, so on
open/email_domainthe invited login is still subject to forced verification. Measured on a real engine: sign-up 200 → sign-in 403EMAIL_NOT_VERIFIED→ the probe reports{present, present}→ the report goes silent.⇒ An operator following this card's primary remedy on a widened posture would have got no login and turned the diagnostic off — the loud-dead-end-becomes-quiet-dead-end transition this card exists to end, delivered by the remedy the card commissioned. The dev's own diagnosis of the error is the clearest statement of it: "a property of ONE posture stated as a property of the carve-out."
⭐ The clause that was wrong was the one clause no pin guarded — 10 pins and 8 mutations, every pin discriminating, none covering that rider. Revision 2 scopes the claim to the default posture, carries the ordering that was measured to work (close a widened posture back to
invite_onlybefore the invited person registers — driven: register underopen→ 403, then close and sign in → 200), aligns all three copies, and pins the scope: mutation M9 replays the F1 defect itself and the new rider-scope pin fires. Pins 10 → 17, mutations 8 → 15, coverage invariant re-measured (every pin reddened by ≥1 mutation, every mutation caught by ≥1 pin).Three pieces of method worth keeping. ⭐ M13 crosses a package boundary and both legs were rebuilt — the suite imports
@objectstack/spec/systemthrough packageexportsto dist/, so a source-only mutation would have measured the pre-mutation artifact; review confirmed it with a leg-0 control that showed exactly that meaningless green (47/47) when dist is not rebuilt. ⭐ The dev's own harness refused one of its mutations — M14's first marker also appeared in the block comment, so the mutation could not be proven to have landed, "a silent no-op that would have read as a passing measurement"; it rescoped and re-ran. ⭐ A gate driver killed by atimeoutwrapper was reported as exit 124 = the kill, not a gate result, and the remaining four gates were re-run separately.Non-blocking, recorded on the PR (comment 5549917008): the PR body had the refusal's direction backwards — it sits upstream of the posture gate, not downstream, which strengthens the claim; and N6, the "no mail transport is needed" clause rests on the platform's default
requireEmailVerificationas well as the posture, so an operator's own explicit setting falsifies it (measured) — it fails safe and was judged not worth holding the PR.Filed and not fixed here: #15718 —
probeSignInAccountsPresenceis existence-only, so one unusablesys_accountrow silences this report permanently. Deliberately out of scope: mutation M5 proved the boundary is real rather than asserted — simulating that fix reddens three pre-existing #14353 pins, so existence-only is pinned as intended behaviour by that card's own suite.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 29, 2026
Found while measuring the recovery path for #14495 (docs card); filed unassigned and unlabeled for triage.
boot-sign-in-reachability.ts(landed for #14353) ends itserrorline with two remedies. Measured on the exact population the report fires on — realObjectQLover better-sqlite3, three humansys_userrows, zerosys_accountrows,NODE_ENV=test, defaultinvite_only— neither remedy does what its sentence says.Remedy (2): "OPEN THE AUDIENCE POSTURE ... so an existing person can register their own login"
An existing person cannot.
email_domain, a seeded person's own registration answers 200 and persists nothing — no row, no account, sign-in still 401 (filed separately as plugin-auth: a sign-up for an address that already has asys_userrow answers 200 and persists nothing (postureemail_domain) #15587). Underinvite_onlythe same call is refused422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL.invite_onlyforces email verification on, so its first sign-in is refused:On a deployment with no mail transport wired — which is the shape a locked-out self-hosted install usually is — remedy (2) therefore produces no login at all.
Remedy (1): "write a
sys_accountcredential row for one of the existingsys_userrows directly against the store"The row shape is public; the format of the secret in its
passwordcolumn is the platform's own. A row carrying a plaintext password authenticates nothing:…and it SILENCES this very diagnostic, because
probeSignInAccountsPresenceasks only whether anysys_accountrow exists:So the operator's first attempt at the named remedy turns the loud dead end back into the silent one it was written to end, with nobody able to sign in.
What does work (measured, same population)
Insert one pending
sys_invitationrow (email— an address the directory does not already hold,status: 'pending', a futureexpires_at,inviter_idof any existingsys_user), then have that person register through the ordinary sign-up endpoint. The invitation carve-out admits that one creation under every posture, no door is widened, no mail transport is needed: sign-up 200, sign-in 200. On thesingleposture the next boot'sbootstrapPlatformAdminthen promotes that account holder (adminPromoted: true).That path is now documented on
content/docs/deployment/self-hosting.mdxby #14495's PR. This card is the runtime half: the message an operator reads at boot should name a remedy that works, and should not describe one whose first step blinds the check.Suggested acceptance
The report's remedy text names the invitation row as the primary way out, states that a hand-written credential row must carry the platform's own password hash (and that writing one silences this report), and states that widening the audience posture forces email verification. Message-text only — no admission semantics move, exactly as #14353 scoped itself.
Refs: #14353 (this diagnostic) · #14349 (option A, ruled) · #14495 (the docs half) · #15587 (the phantom 200).
Generated by Claude Code