docs(self-hosting): name the measured recovery for a directory-seeded, credential-less deployment - #15586
Conversation
… deployment The page told an operator on a walled posture to "have each of those people register with exactly that address". Measured on a real kernel (three human `sys_user` rows, zero `sys_account` rows, outside development, default `invite_only` audience posture), that registration is refused `403 SELF_REGISTRATION_CLOSED`: the first-account carve-out counts humans, not logins, and humans already exist. So the published recovery path was the one that is shut, and the deployment cannot be recovered from inside. Both first-boot paths now say what "fresh" means, and a new subsection states the dead end, points at the boot diagnostic (`no_sign_in_account_at_boot`), and names the measured way out: write one pending `sys_invitation` row against the store for an address the directory does not already hold, then have that person register normally — measured sign-up 200 and sign-in 200 with no mail transport, and on `single` the next boot promotes that account holder (`adminPromoted: true`) rather than a credential-less directory row. Two plausible-looking remedies are documented as measured failures: opening the audience posture answers 200 while persisting nothing for an address the directory already holds, and forces email verification on every other one; and a hand-written `sys_account` row with a plaintext password authenticates nothing while silencing the boot diagnostic. The dev-admin seed is described as development-only, as it is gated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…lf-hosting-bootstrap-recovery
|
ACCEPT — PM seat Reviewed against the three-dot diff What I checked in the diff:
Dev's verification quoted: measurement harness Findings filed by the dev, bare for triage: #15587 (sign-up for an already-held address answers 200 and persists nothing under Flipping ready + enabling auto-merge. Fixes #14495. Generated by Claude Code |
Fixes #14495
content/docs/deployment/self-hosting.mdxtold an operator on a walled postureto "have each of those people register with exactly that address". On a
deployment whose people directory was seeded without credentials that
registration is the door
invite_onlyshuts, so the published recovery pathwas the one that is closed.
Docs-only. No image-tag version rows touched, nothing under
content/docs/releases/.The measurement (acceptance 1) — done first, not assumed
Real kernel:
ObjectQLover@objectstack/driver-sql+ better-sqlite3:memory:, the auth plugin's ownauthIdentityObjects, driven throughAuthManager.handleRequest(the HTTP lane) — the harness shapeaudience-bootstrap-seam.test.tsuses, because the in-memory doubles cannotanswer this (#4419
requireFindOnePredicate). Population: three humansys_userrows, zerosys_accountrows,NODE_ENV=test(notdevelopment,so the dev-admin seed is disarmed by
isDevAdminSeedArmed()), defaultinvite_onlyaudience posture.Baseline — the dead end, reproduced:
The boot diagnostic from #14353 (CLOSED — landed as
packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts) fires onexactly this population, under the grep token
no_sign_in_account_at_boot,at
errorlevel; the page now points at it.The working path (this is what the page now documents). Write one pending
sys_invitationrow against the store —email(an address the directory doesNOT already hold),
status: 'pending', a futureexpires_at,inviter_idofany existing
sys_userrow — and have that person register through theordinary sign-up endpoint. The invitation carve-out admits that one creation
under every posture, so the
invite_onlydefault is never widened:…and the recovered account is the one first-boot promotion finds, on the
singleposture — measured by runningbootstrapPlatformAdminover the sameengine afterwards:
i.e. the grant lands on the invited account holder, never on a
credential-less directory row (#14348's selector).
Two remedies the boot diagnostic names that do NOT produce a login on their
own — measured, and documented on the page as failures:
Two further facts the page states:
422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL(invitation or not), so theinvited address must be a new one;
OS_PLATFORM_OWNER_EMAILand read verified — measured,shouldStampOwnerVerifiedAtCreationanswersfalsefor aninvitation-admitted (self-serve, non-bootstrap) creation and
trueonly fora bootstrap or operator-class one, so that address needs a mailbox.
Acceptance 3 (stop rule) does not fire: a working path exists and is measured,
so
premise_still_validis true.What changed on the page (acceptance 2)
singleparagraph now says what fresh means — zero humansys_userrows, not merely zero logins — and links to the new subsection;
directory and points at the out-of-band recovery;
measured refusal table, the boot diagnostic and its grep token, the
invitation row that recovers the deployment, the two measured non-remedies,
and the prevention (seed logins with the directory);
NODE_ENV=development-gated —a development convenience, never a production recovery path.
Verification
Gate family derived, not guessed:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack— re-derived after merging
origin/mainso the answer is not from a staletree; change set
1 path(s) vs merge base 3d3f60e01, exactlycontent/docs/deployment/self-hosting.mdx. All 38 derived commands run,each exit code captured before any pipe; 38/38 exit 0. Verdict lines the
gates printed:
Two gates first answered
PREREQUISITE NOT METrather than a verdict —check:docs("packages/spec/json-schema is older than packages/spec/src") andcheck:skill-examples(spec.d.tsstale, then"packages/client-react/dist holds no .d.ts declarations"). Those are NOT
MEASURED, never a pass:
pnpm --filter @objectstack/spec buildandpnpm --filter @objectstack/client-react... buildwere run (bothos-verify-lock: VERDICT command-exit 0) and both gates re-run to the greenverdict lines quoted above.
Heavy runs went through the shared entry point
(
bash scripts/pm/os-verify-lock.sh -c …, slotissue-14495-dev); themeasurement itself ended
os-verify-lock: VERDICT command-exit 0 · held the lock 16swithTests 6 passed (6).Repo-wide
pnpm lint(eslint . --no-inline-config) is CI's run, and thisdiff is provably outside its population — a declared narrowing, with its three
pieces: (1) eslint's own config resolution answers, for the only changed file,
File ignored because no matching configuration was supplied; (2)--format jsonreturns 1 result, 0 errors; (3) the diff adds no eslint configand no JS/TS source, so no untouched file's verdict can move.
Every figure above was measured on the branch's final tree,
git rev-parse --short HEAD=472e204e1.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code