Skip to content

Commit 67ee171

Browse files
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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/console': patch
3+
---
4+
5+
fix(console): the boot's `get-session` starts from `index.html`'s inline script beside `runtime/config`, and the preflight reuses the answer
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Console's pre-React auth preflight validates the stored Bearer with `GET /api/v1/auth/get-session`, and its answer is the session `AuthProvider` starts from. It was sent from the main bundle, so it waited for that bundle to download and evaluate, a whole round trip behind the parse-time `runtime/config` prefetch, at the head of the startup chain.
11+
12+
`index.html` now carries a second parse-time script, `startEarlySessionCheck`, that sends the preflight's exact request (same URL, same `Authorization: Bearer` from `localStorage['auth-session-token']`, `credentials: 'include'`, same `Accept`) right after `runtime/config`, and leaves the pending answer under `Symbol.for('objectui.earlySessionCheck')` with the auth URL and token it sent. `preflightAuth()` takes it once when both match and reads it instead of asking again, so a page load sends one `get-session`, started during parse.
13+
14+
Every existing safety holds: no stored token or blocked storage sends nothing; a failed or non-OK early answer is not reused and the preflight fetches as before; a stale token answered early is purged as before; a token that changed since parse time is asked about again.

‎ui/apps/console/index.html‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,66 @@
123123
} catch (_) { /* non-blocking */ }
124124
})();
125125
</script>
126+
<!--
127+
The page's ONE `GET /api/v1/auth/get-session`, started during parse
128+
beside `runtime/config` above (objectstack#29118).
129+
130+
`preflightAuth()` in `src/lib/auth-preflight.ts` validates the stored
131+
Bearer before React renders, and it is the boot's only session read
132+
(`AuthProvider` takes its answer, objectui#12808). Asked from the bundle,
133+
it waited for the main chunk to download and evaluate: a whole round
134+
trip at the head of the startup chain that needs nothing from the bundle.
135+
This script asks the SAME question at parse time and hands the answer to
136+
the preflight, the way the script above hands `runtime/config` to
137+
`initRuntimeConfig()`.
138+
139+
Same question means same answer, so the request is the preflight's
140+
byte for byte: the same URL, the same `Authorization: Bearer` read from
141+
the same `localStorage` key, `credentials: 'include'` (the cookie rides
142+
along so a renewed session cookie is stored, objectui#12892) and the
143+
same `Accept`. The auth URL is spelled as `src/main.tsx`'s `AUTH_URL`,
144+
through the same Vite HTML env substitution, and the slot carries that
145+
URL and the token it sent; the preflight takes it once, and only when
146+
both match what it would send itself.
147+
148+
Every safety of the preflight is kept. No token, or blocked storage,
149+
sends nothing, exactly as the preflight returns early. A request that
150+
fails or answers non-OK hands over a rejection, so the preflight fetches
151+
as it always did and the stale-token purge reads its own answer.
152+
153+
`src/__tests__/sessionCheckBootDedup.test.tsx` executes THIS script's
154+
text against the real `preflightAuth()`, so a drift between the two
155+
spellings of the slot fails a test rather than silently restoring the
156+
second request.
157+
-->
158+
<script>
159+
(function startEarlySessionCheck() {
160+
let token;
161+
try {
162+
token = localStorage.getItem('auth-session-token');
163+
} catch (_) {
164+
return; // storage blocked: the preflight has nothing to validate either
165+
}
166+
if (!token) return;
167+
168+
// `src/main.tsx`: AUTH_URL = (VITE_SERVER_URL or '') + '/api/v1/auth'.
169+
// An unsubstituted token reads as same-origin '', as in the script above.
170+
const injected = '%VITE_SERVER_URL%';
171+
const authUrl = (injected.charAt(0) === '%' ? '' : injected) + '/api/v1/auth';
172+
173+
const body = fetch(authUrl + '/get-session', {
174+
method: 'GET',
175+
credentials: 'include',
176+
headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },
177+
}).then(function (res) {
178+
if (!res.ok) throw new Error('get-session ' + res.status);
179+
return res.json().catch(function () { return null; });
180+
});
181+
// The preflight reads a failure as "ask again"; it is never unhandled.
182+
body.catch(function () {});
183+
globalThis[Symbol.for('objectui.earlySessionCheck')] = { authUrl: authUrl, token: token, body: body };
184+
})();
185+
</script>
126186
<script>
127187
window.process = window.process || { env: { NODE_ENV: 'development' }, version: '', platform: 'browser' };
128188
</script>

0 commit comments

Comments
 (0)