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
{{ message }}
Repository navigation
Commit 33b6e8b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-auth): a host declares its own sign-in handoff, and no_sign_in_account_at_boot stops calling that deployment a dead end (#20861) (#20893)
Fixes#20861
Clause-②: yes (widening)
## What this changes
The `no_sign_in_account_at_boot` boot report already stays quiet when a
deployment has a delegated sign-in path. Since #15074 that means
`ssoOnlyMode`, a configured social or OIDC provider, or enterprise SSO
with a registered IdP. All three are read off `getPublicConfig()`, which
is what the login page is told. A sign-in route the **host** owns, and
the login page does not show, was invisible to the gate. So a hosted
kernel whose owner hid the platform sign-in button logged the ERROR on
every rebuild while that owner signed in through the host's handoff.
Following triage's direction (5912860738), `SignInPathWiring` gains ONE
declared fact, and the host sets it:
- `AuthPluginOptions.hostSignInHandoff?: boolean` (default `false`).
This is the only way to state it: no env var, no setting.
- The `kernel:ready` hook hands the plugin's own options to
`probeSignInPathWiring` as a fourth argument (`SignInPathHostView`). The
gate then treats the fact like the other delegated paths: no `error`,
and a `debug` line under the same grep token that names
`hostSignInHandoff` as the reason.
- Only a literal `true` declares it; any other value reads as not
declared, which is the loud direction. A declared handoff also skips the
`sys_sso_provider` read, like any path proven without the store.
- ⛔ No inference. Nothing reads environment names,
`platform_sso_enabled` or row counts. The host states what it wires.
- ⛔ The public config does not lie. The option registers no provider,
and `getPublicConfig()` returns the same value with or without it
(pinned).
- ⛔ Not a remedy. The ERROR text does not mention the option. The
operator of a locked-out self-hosted deployment reads that line, and
offering a one-word switch that silences it would turn a loud dead end
into a silent one.
- A deployment that does not declare the fact reports exactly as before,
including every self-hosted deployment with no delegated path. The
#14353 suite (`boot-sign-in-reachability.test.ts`) is unedited and
green.
## For the cloud follow-up
Where the hosted kernel constructs `AuthPlugin` and mounts the owner
handoff (`sso_as_owner` to `sso-handoff-issue` to `sso-exchange`),
declare `new AuthPlugin({ /* existing options */ hostSignInHandoff: true
})`. The mutation leg of objectstack-ai/cloud#2504
(`platform_sso_enabled` false) should turn green once that is in place.
## Mechanism assumptions, measured (B1 to B6)
- **B1, holds.** The gate lives in
`packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts`:
`resolveDelegatedSignInPath` (:414 on base),
`resolveNoSignInAccountReport` (:459) and
`reportIfNoSignInAccountExists` (:548). Before this PR,
`probeSignInPathWiring` (:388) resolved the three facts from
`SignInPathConfigView`. There is one call site, the `kernel:ready` hook
in `auth-plugin.ts`: `pub` comes from
`this.authManager.getPublicConfig()` (:1025), and the gate is called at
:1055 and :1056.
- **B2, the idiom.** A host states wiring facts as `AuthPluginOptions`
constructor options: `manifestDatasource`, `databaseHooks`, and
`membershipPolicy`. The organizations boot text says hosts declare
`membershipPolicy` in the `AuthPlugin` constructor, "what the cloud
control plane does". `os serve`
(`packages/cli/src/commands/serve.ts:3991`) passes explicit fields and
spreads no authored config into the plugin. So the option is reachable
only by code that constructs `AuthPlugin`, which is a host. The option
has one name and is read from one place. An env var would be a second
spelling, and it would let an operator declare a route that nothing
mounts.
- **B3, holds.** `getPublicConfig()` (`auth-manager.ts:6599` to `:6799`)
builds every field explicitly and never spreads `this.config`, so a new
plugin option cannot leak into it. A pin compares the full return value
with and without the declaration. Nothing needed to change there.
- **B4.** See the one-liner above. Nothing is filed in another
repository.
- **B5, `yes (widening)`.** `AuthPluginOptions` is re-exported by the
`.` entry (`export * from './auth-plugin.js'`). After `pnpm --filter
@objectstack/plugin-auth build`, `dist/index.d.ts` carries
`hostSignInHandoff?: boolean` at line 211. That line is inside
`interface AuthPluginOptions` (lines 119 to 212), and `files` is
`["dist","README.md","CHANGELOG.md"]`. `SignInPathWiring`,
`SignInPathHostView` and `probeSignInPathWiring` occur 0 times in both
`dist/index.d.ts` and `dist/rate-limit-storage.d.ts`, so the gate module
stays unpublished. Positive control on the same grep: `AuthPlugin`
occurs 40 and 1 times. The changeset is graded `minor`.
- **B6.** `content/docs/deployment/self-hosting.mdx` documents the boot
report. It now says when the same shape is logged at `debug` instead,
including the new declaration, and warns that setting the option without
a real handoff route hides the dead end. No docs page lists
`AuthPlugin`'s host options, so there was nothing else to update.
## Evidence
Pins first, then the fix, then an ablation. All pins are in
`boot-sign-in-reachability.sso-gate.test.ts`.
- **Red, pins only (`f6be56009`, source at base):** `Tests 8 failed | 26
passed (34)`. Six are the new #20861 pins. Two are the existing shape
pins, which now expect the four-fact `NOTHING_WIRED`. Every failure is
an `AssertionError`, for example `expected [ Array(1) ] to have a length
of +0 but got 1` on the declared boot. The CONTROL pin and the
`getPublicConfig()` pin are green, which is correct: they hold both
before and after the fix.
- **Green, fix (`88aff6fe7`):** the gate suite plus the #14353 suite
gives `Test Files 2 passed (2)` and `Tests 85 passed (85)`, against a
baseline of 77 at `9905e61ca`.
- **Ablation (`88aff6fe7`):** deleted the `if
(wiring.hostSignInHandoff)` branch in `resolveDelegatedSignInPath`
through `scripts/ablation-replace.mjs`, in WRAP mode with an
absolute-path trap. Anchor count went 1 to 0; blob went `cf6dd010f040`
to `67310dc47389`. Result: `Tests 3 failed | 31 passed (34)`, the three
predicted pins (the declared boot, the `debug` line, the gate-level
null). The resolver pins stay green because they test the resolver, not
the branch. Restore: blob after `cf6dd010f040` equals the HEAD blob, and
`git diff HEAD` and `git status --porcelain` are empty. The subject is
imported relatively (`./boot-sign-in-reachability`, which is `src`), so
no `dist/` sits on the resolution path.
- **Package (`88aff6fe7`):** `pnpm --filter @objectstack/plugin-auth
test` gives `Test Files 115 passed (115)` and `Tests 2472 passed
(2472)`. `typecheck` exits 0, including `check:test-typecheck` OK.
- **Gates (`88aff6fe7`):** `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 95 commands. I ran all 95, plus
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`
(`check:auth-mount-ledger` is in the 95). `--ran` answered `95 derived
famil(ies) accounted for — 95 run, 0 NOT-MEASURED (a DERIVED zero …)`.
Three gates first exited 3 (`PREREQUISITE NOT MET`):
`check:skill-examples`, `check:type-check-debt` and
`check:dual-build-cjs-loads`. After I built the
`@objectstack/client-react` closure, all three exited 0 on re-run.
`check:type-check-debt` reported "4 ledger entries re-measured … none
above its recorded number", and `check:dual-build-cjs-loads` reported
"105 published require entry points across 66 packages load".
- **Lint (narrowed, `88aff6fe7`):** eslint `--no-inline-config --format
json` over the three touched TS files checked 3 files with 0 errors and
0 warnings. The config itself says the two Markdown paths are "File
ignored because no matching configuration was supplied", so those 3
files are the diff's whole lint population. Type-aware linting is off
(`eslint.config.mjs:327`, and no `parserOptions.project`), and the diff
touches no lint config or rule, so it cannot change any untouched file's
lint result. The repo-wide `pnpm lint` is left to CI.
## Acceptance notes
- Noted, not filed: the walled-owner neighbour
(`warnIfWalledOwnerCannotVerify`) decides `hasFederatedSignIn` from
`pub` only and does not read `hostSignInHandoff`. That is by #15074's
design ("the neighbour is untouched by this gate", pinned). Whether a
hosted kernel with the button hidden also meets that neighbour's four
preconditions is NOT MEASURED here. Carrier: the cloud follow-up card,
whose rebuild log will show it.
- `main` moved five commits past the base (`9905e61ca` to `4edb61449`)
while this ran. None of them touches this PR's five paths (measured).
Per the dispatch's same-day-churn clause, no merge was made. CI's merge
ref checks the combined tree.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(plugin-auth): a host declares its own sign-in handoff route with `hostSignInHandoff`, and the `no_sign_in_account_at_boot` boot report stops calling that deployment a dead end (#20861)
6
+
7
+
Clause-②: yes (widening)
8
+
9
+
`AuthPluginOptions` gains one option, `hostSignInHandoff?: boolean` (default
10
+
`false`). The HOST that constructs the plugin sets it when it signs people in
11
+
through a handoff route of its own: a route that is not a login-page provider
12
+
and that creates the session without writing a `sys_account` row. A hosted
13
+
kernel whose owner signs in through the control plane is the case it is for.
14
+
That owner can still sign in when the login page shows no platform sign-in
0 commit comments