Repository navigation
Commit 67ee171
fix(console): start the boot get-session from index.html's inline script; the preflight reuses the answer (#29207)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb5v4M6zfLKgS9L1ViwTRh3z)_
Fixes #29118
Clause-②: no
Metadata-protocol: none
Claim: issue comment 6572644370 (branch `claude/project-thread-9cdi3p`,
session `session_01QsR2jSnFx895cTQzM4QL45`).
## 维护者速读
- **改了什么**:打开控制台时,校验会话的 `get-session` 请求现在由 `index.html` 的内联脚本在解析 HTML
时就发出,紧跟在已有的 `runtime/config`
预取之后;主包里的预检(`preflightAuth`)拿这次的答案用,不再自己再发一次。
- **为什么改**:以前这条请求要等主 JS 包下载并执行完才发,白白排在启动链最前面多等一个往返。实测(构建产物、本地限速 150 ms
RTT / 1.6 Mbps):`get-session` 与 `runtime/config` 的发出间隔从约 38.8 秒降到 14
毫秒;不限速时从约 447 毫秒降到 1 毫秒。每次加载仍只有一个 `get-session`。
- **风险与回滚**:低。早发请求与预检原本发的请求逐字节相同(同 URL、同 Bearer、`credentials:
'include'`、同 Accept),预检只在 URL 与令牌都一致时才复用;无令牌、存储被禁时不发;早发请求失败或非 2xx
时不复用,预检照旧自己请求;过期令牌照旧清除。回滚即还原本 PR。
- **先看**:`ui/apps/console/index.html` 新增的 `startEarlySessionCheck`
脚本;`ui/apps/console/src/lib/auth-preflight.ts` 的
`takeEarlySessionAnswer`;新测试 `sessionCheckBootDedup.test.tsx`。
- **两分钟验证**:在 `ui/` 下运行 `pnpm exec vitest run
apps/console/src/__tests__/sessionCheckBootDedup.test.tsx`,预期 1 个文件、18
个测试全绿。
- **需要你做的**:无,席位落地。
## What changes
- **`ui/apps/console/index.html`**: a second parse-time classic script,
`startEarlySessionCheck`, placed directly after the `runtime/config`
prefetch. It reads `localStorage['auth-session-token']` (blocked storage
or no token: it sends nothing, as the preflight returns early), builds
the auth URL exactly as `src/main.tsx`'s `AUTH_URL` (`VITE_SERVER_URL`
through the same Vite HTML env substitution, unsubstituted reads as
same-origin), and sends the preflight's request: `GET
{authUrl}/get-session`, `credentials: 'include'`, `Authorization: Bearer
TOKEN`, `Accept: application/json`. It leaves `{ authUrl, token, body }`
under `Symbol.for('objectui.earlySessionCheck')`, where `body` is the
parsed answer of an OK response and rejects for a failed or non-OK one.
- **`ui/apps/console/src/lib/auth-preflight.ts`**: `preflightAuth()`
takes that slot once (`takeEarlySessionAnswer`) when its auth URL and
token equal what it would send, and reads the answer instead of asking.
Anything else (no slot, a second call, a mismatch, a rejection) falls
through to `askSession`, the request it always sent, unchanged. What it
does with the answer is unchanged: a user hands the body on to
`AuthProvider` (objectui#12808); a non-OK or null answer purges the
token and the active-organization keys; a network error leaves storage
alone.
- No change to `main.tsx`, `@object-ui/auth`, any metadata key, option
or component.
**The card says the check needs only the cookie; the request also
carries the Bearer.** The early request is the preflight's, header for
header: the stale-Bearer check is the preflight's whole job (better-auth
reads the Bearer over the cookie), so a cookie-only early request could
answer differently from the request it replaces. The test "asks exactly
what the preflight would ask" runs both and compares URL, method,
credentials and every header entry.
## Tests
- **New: `src/__tests__/sessionCheckBootDedup.test.tsx`, 18 cases.** It
extracts the shipped inline scripts from `index.html` and executes them,
then runs the real `preflightAuth()` and, end to end, the real
`AuthProvider` over one stubbed `fetch`.
- Findable: exactly one `startEarlySessionCheck` inline script; a
missing one throws rather than passing vacuously; `main.tsx` still
spells `AUTH_URL` the way the script copies it and hands it to
`preflightAuth`.
- One page load, one `get-session`: running the two parse-time scripts
alone (no microtask, no bundle) already records `runtime/config` then
`get-session`; the preflight reads the in-flight answer, and the settled
one, with one request; the answer still reaches `AuthProvider`'s slot;
script, preflight and a StrictMode `AuthProvider` together send one
`get-session`.
- Safeties: no token sends nothing (script and preflight); blocked
storage sends nothing and does not throw; a 503 or a network failure at
parse time is not reused and the preflight fetches once itself (2
total), deciding from its own answer; a stale token answered `200 null`
at parse time is purged with no second ask; a token changed since parse
time is asked about again; the answer is taken once.
- Origin: `VITE_SERVER_URL` substituted (with and without a trailing
slash) joins the preflight built from the same value; unset falls back
to same-origin, never the raw token.
- **Ablation** (preflight ignores the early answer:
`takeEarlySessionAnswer(...) ??` removed through
`scripts/ablation-replace.mjs`): 7 failed, 11 passed, every failure the
request count (`expected … to have a length of 1 but got 2`, and 3 for
the taken-once case). Restored by the tool: blob equal to `HEAD`, `git
diff HEAD` empty. The first attempt used a replacement that was a
substring of the anchor and was refused by the tool before running
(no-op, restored); the second carried a unique marker and landed.
## Acceptance notes
- **Measured on a built Console, against a stub server, not `objectstack
dev --ui`.** `vite build` of the Console at this branch and with
`index.html` and `auth-preflight.ts` restored from base `0d6d4061`, each
served by a small Node server answering `runtime/config` and
`get-session` with JSON, a stored token seeded before load, Playwright
Chromium, resource-timing `startTime`, 5 loads each, cache off.
- Throttled (150 ms RTT, 1.6 Mbps down): `get-session` started
38,729–38,863 ms after navigation start (median gap to `runtime/config`
38,774 ms) before; 201–208 ms, median gap 14 ms after.
`DOMContentLoaded` at ~38.9 s in both.
- Unthrottled: median gap 447 ms before, 1 ms after; `get-session`
started ~450 ms before `DOMContentLoaded` after (before: 1 ms ahead of
it, the module chunk's own tick).
- One `get-session` per load in both builds (server-side count).
- **A cookie-only boot (no stored Bearer) is unchanged.** The preflight
sends nothing then, and `AuthProvider` asks after mount; the claim
scoped the early request to the preflight's request. Sign-in and every
session read store the token (`TokenStorage.set` in `@object-ui/auth`),
so the token path is the ordinary one.
- **A non-OK early answer costs a second request** by design (the
claim's safety rule): the preflight then asks itself, as it did before.
Today the preflight treats any non-OK answer, a 5xx included, as stale
and purges; a transient 5xx at parse time now gets one more ask before
that verdict. better-auth answers an unknown session `200 null`, which
is reused as is.
## Verification
All readings on head `2920a780` unless named; merged `origin/main`
`0d15f895` as `9de49630` (no incoming change under `ui/apps/console` or
`ui/packages/auth`), and the eight boot suites re-ran green there.
<details><summary>Tests</summary>
- From `ui/`, locked, `--maxWorkers=2`: `sessionCheckBootDedup`,
`sessionBootDedup`, `authPreflightCredentials-12892`,
`runtimeConfigBootDedup`, `insecure-origin-crypto.placement`,
`insecure-origin-crypto`, `bootSplash`, `zod-jitless-13511`: 8 files, 75
tests passed (on `2920a780` and again on `9de49630`).
- Ablation run: 1 file, 7 failed, 11 passed (see Tests).
- `pnpm exec turbo run type-check --filter=@object-ui/console
--concurrency=2`: 71 of 71 tasks successful (the console `tsconfig.json`
includes `src`, so the new test is compiled).
- `eslint` on `auth-preflight.ts` and the new test: exit 0.
</details>
<details><summary>Gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 33 families; all 33 ran with exit 0,
including `check-sdui-manifest` (and `--self-test`),
`check-changeset-no-major`, `check-adr-0087-registration`,
`check-empty-changeset`, `check:nul-bytes`, `check:test-source-alias`,
`check:cross-package-test-inputs`, and `pnpm --dir ui exec vitest run
scripts/` locked: 169 files passed, 2 skipped (4,900 tests). `pnpm
check:changeset-fixed` (roster under `.changeset`): exit 0.
- `dispatch-gates --ran`: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.
- PR-body gates on this body: `check-changeset-no-major`,
`check-adr-0087-registration`, `check-closing-keyword-parity`,
`check-partof-closing-keyword`.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jSnFx895cTQzM4QL45
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jSnFx895cTQzM4QL45)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 987ef87 commit 67ee171
4 files changed
Lines changed: 488 additions & 13 deletions
File tree
- .changeset
- ui/apps/console
- src
- __tests__
- lib
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
126 | 186 | | |
127 | 187 | | |
128 | 188 | | |
| |||
0 commit comments