-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(web-shell): preserve token and base path in session URLs #7926
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cf4826b
ca58b2e
eb117ca
0536742
352fd88
16c8daf
02a08ee
6b9f7f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { | |
| } from './config/daemon'; | ||
| import { normalizeLanguage, type WebShellLanguage } from './i18n'; | ||
| import { WebShellThemeId, type WebShellTheme } from './themeContext'; | ||
| import { buildSessionPathname, parseSessionId } from './utils/sessionPath'; | ||
| import 'katex/dist/katex.min.css'; | ||
| import './styles/standalone.css'; | ||
|
|
||
|
|
@@ -78,13 +79,7 @@ function getInitialLanguage(): WebShellLanguage { | |
| } | ||
|
|
||
| function getSessionIdFromUrl(): string | undefined { | ||
| const match = window.location.pathname.match(/\/session\/([^/]+)/); | ||
| if (!match) return undefined; | ||
| try { | ||
| return decodeURIComponent(match[1]); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| return parseSessionId(window.location.pathname); | ||
| } | ||
|
|
||
| function getWorkspaceIdFromUrl(): string | undefined { | ||
|
|
@@ -98,7 +93,7 @@ function replaceStandaloneSessionUrl( | |
| workspaceId?: string, | ||
| ): void { | ||
| const url = new URL(window.location.href); | ||
| url.pathname = sessionId ? `/session/${encodeURIComponent(sessionId)}` : '/'; | ||
| url.pathname = buildSessionPathname(url.pathname, sessionId); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The wiring in Suggested fix: add a small test around 中文说明[Suggestion] 建议修复:为 — kimi-k3 via Qwen Code /review
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partially addressed. The behavior this PR changes — building the session pathname under a base path and reading it back — is now covered by pure unit tests in I declined the specific ask to unit-test the 中文说明部分处理。本 PR 改动的行为——在基础路径下构建会话路径并回读——现在已由 我拒绝了其中的具体诉求(对
qwen-code-dev-bot marked this conversation as resolved.
|
||
| if (sessionId && workspaceId) { | ||
| url.searchParams.set('workspace', workspaceId); | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Qwen Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { describe, expect, it } from 'vitest'; | ||
| import { buildSessionPathname, parseSessionId } from './sessionPath'; | ||
|
|
||
| describe('buildSessionPathname', () => { | ||
| it('replaces an existing session segment at the root', () => { | ||
| expect(buildSessionPathname('/session/old', 'new')).toBe('/session/new'); | ||
| }); | ||
|
|
||
| it('preserves a sub-path deployment base', () => { | ||
| expect(buildSessionPathname('/app/session/old', 'new')).toBe( | ||
| '/app/session/new', | ||
| ); | ||
| }); | ||
|
|
||
| it('appends a session under a base path with no existing session', () => { | ||
| expect(buildSessionPathname('/app', 'new')).toBe('/app/session/new'); | ||
| }); | ||
|
|
||
| it('appends a session at the root when there is no existing session', () => { | ||
| expect(buildSessionPathname('/', 'new')).toBe('/session/new'); | ||
| }); | ||
|
qwen-code-dev-bot marked this conversation as resolved.
|
||
|
|
||
| it('strips a trailing slash from the base path', () => { | ||
| expect(buildSessionPathname('/app/', 'new')).toBe('/app/session/new'); | ||
| }); | ||
|
|
||
| it('strips a trailing slash after an existing session id', () => { | ||
| expect(buildSessionPathname('/session/old/', 'new')).toBe('/session/new'); | ||
| expect(buildSessionPathname('/app/session/old/', 'new')).toBe( | ||
| '/app/session/new', | ||
| ); | ||
| }); | ||
|
|
||
| it('encodes the session id', () => { | ||
| expect(buildSessionPathname('/', 'a b/c')).toBe('/session/a%20b%2Fc'); | ||
| }); | ||
|
|
||
| it('returns the base path when no session is given', () => { | ||
| expect(buildSessionPathname('/app/session/old', undefined)).toBe('/app'); | ||
| }); | ||
|
|
||
| it('returns "/" when no session is given at the root', () => { | ||
| expect(buildSessionPathname('/', undefined)).toBe('/'); | ||
| expect(buildSessionPathname('/session/old', undefined)).toBe('/'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('parseSessionId', () => { | ||
| it('reads the session id at the root', () => { | ||
| expect(parseSessionId('/session/abc')).toBe('abc'); | ||
| }); | ||
|
|
||
| it('reads the last session segment under a base path', () => { | ||
| expect(parseSessionId('/app/session/abc')).toBe('abc'); | ||
| }); | ||
|
|
||
| it('ignores a trailing slash', () => { | ||
| expect(parseSessionId('/session/abc/')).toBe('abc'); | ||
| }); | ||
|
|
||
| it('decodes the session id', () => { | ||
| expect(parseSessionId('/session/a%20b%2Fc')).toBe('a b/c'); | ||
| }); | ||
|
qwen-code-dev-bot marked this conversation as resolved.
|
||
|
|
||
| it('returns undefined for malformed percent-encoding', () => { | ||
| expect(parseSessionId('/session/%E0%A4%A')).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns undefined when there is no session segment', () => { | ||
| expect(parseSessionId('/')).toBeUndefined(); | ||
| expect(parseSessionId('/app')).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns undefined for an empty session id', () => { | ||
| expect(parseSessionId('/app/session/')).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('build/parse round-trip', () => { | ||
| it('reads back the written session id', () => { | ||
| for (const base of ['/', '/app', '/app/', '/app/session/old']) { | ||
| expect(parseSessionId(buildSessionPathname(base, 'real-id'))).toBe( | ||
| 'real-id', | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it('reads back the written id when the base path ends in a session segment', () => { | ||
| const pathname = buildSessionPathname('/app/session/', 'real-id'); | ||
| expect(parseSessionId(pathname)).toBe('real-id'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Qwen Team | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| /** | ||
| * Build the pathname for a standalone session URL while preserving any base | ||
| * path the app is deployed under (e.g. `/app/session/<id>` stays under | ||
| * `/app` instead of being reset to `/session/<id>`). With no session id, | ||
| * returns the base path (or `/` at the root). | ||
| */ | ||
| export function buildSessionPathname( | ||
| currentPathname: string, | ||
| sessionId: string | undefined, | ||
| ): string { | ||
| const sessionPath = currentPathname.match(/^(.*)\/session\/[^/]+\/?$/); | ||
|
qwen-code-dev-bot marked this conversation as resolved.
|
||
| const basePath = sessionPath?.[1] ?? currentPathname.replace(/\/$/, ''); | ||
|
qwen-code-dev-bot marked this conversation as resolved.
|
||
| return sessionId | ||
| ? `${basePath}/session/${encodeURIComponent(sessionId)}` | ||
| : basePath || '/'; | ||
| } | ||
|
|
||
| /** | ||
| * Extract the session id from a standalone pathname. Anchored to the last | ||
| * `/session/<id>` segment so it agrees with `buildSessionPathname`'s greedy | ||
| * writer; a first-match parse would read the literal `session` segment when | ||
| * the base path itself ends in `/session` (e.g. `/app/session/session/<id>`). | ||
| */ | ||
| export function parseSessionId(pathname: string): string | undefined { | ||
| const match = pathname.match(/\/session\/([^/]+)\/?$/); | ||
| if (!match) return undefined; | ||
| try { | ||
| return decodeURIComponent(match[1]); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The PR title, description, Reviewer Test Plan, and risk note are stale after commit
ca58b2eac— they still claim the token "remains in the URL" (title: "preserve token and base path"; test plan: "confirm the token remains in the URL"; After example:/web-shell/session/new#token=secret; risk: "The token intentionally remains visible in the browser address bar and history"), but the code strips it again (removeDaemonTokenFromUrl()at startup andurl.searchParams.delete('token')on every session-URL rewrite, both in non-DEV), so the net change is now only the base-path fix — Failure scenario: a reviewer follows the documented test plan in a production build, opens the shell with?token=, switches sessions, and observes the token being stripped, contradicting the title, test plan, After-evidence, and the stated security tradeoff.Suggested fix: update the PR title and description to drop the token-retention claims and reframe the PR as the base-path preservation fix (remove the token-related verification steps, the After example with
#token=, and the token-visibility risk note).中文说明
[Suggestion] PR 标题、描述、Reviewer Test Plan 和风险说明在提交
ca58b2eac之后已经过时——它们仍声称 token 会“保留在 URL 中”(标题:“preserve token and base path”;测试计划:“confirm the token remains in the URL”;修改后示例:/web-shell/session/new#token=secret;风险:“The token intentionally remains visible in the browser address bar and history”),但代码已恢复移除 token 的逻辑(启动时调用removeDaemonTokenFromUrl(),每次会话 URL 重写时执行url.searchParams.delete('token'),均在非 DEV 下),因此当前净改动只剩基础路径保留修复——失败场景:评审者按照文档中的测试计划在生产构建中用?token=打开 Web Shell 并切换会话,观察到 token 被移除,与标题、测试计划、修改后示例以及所述安全取舍相矛盾。建议修复:更新 PR 标题和描述,去掉 token 保留相关的表述,将 PR 重新定位为基础路径保留修复(删除 token 相关的验证步骤、带
#token=的修改后示例以及 token 可见性的风险说明)。— kimi-k3 via Qwen Code /review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed — the PR text is stale. After
ca58b2eacthe net change is the base-path preservation fix; the token is still stripped (removeDaemonTokenFromUrl()at startup andsearchParams.delete('token')on every session-URL rewrite, both in non-DEV), so the title, test plan, After example, and token-visibility risk note no longer match the code.I have no GitHub credentials and cannot edit the PR title/body from here — the workflow owns network writes. I have written a corrected, reframed
pr-title.txt(fix(web-shell): preserve deployment base path in standalone session URLs) andpr-body.md(base-path fix; token handling described as unchanged, with the token-retention verification steps and risk note removed) to the workdir for the workflow/maintainer to apply. Leaving this thread open because I cannot verify the PR text was actually updated.中文说明
同意——PR 文本确实已过时。在
ca58b2eac之后,净改动是基础路径保留修复;token 仍会被移除(启动时调用removeDaemonTokenFromUrl(),每次会话 URL 重写时执行searchParams.delete('token'),均在非 DEV 下),因此标题、测试计划、修改后示例以及 token 可见性的风险说明已不再与代码相符。我没有 GitHub 凭据,无法在此处编辑 PR 标题/正文——网络写入由工作流负责。我已将更正后、重新定位的
pr-title.txt(fix(web-shell): preserve deployment base path in standalone session URLs)和pr-body.md(基础路径修复;将 token 处理描述为未改动,并删除了 token 保留相关的验证步骤和风险说明)写入工作目录,供工作流/维护者应用。该线程保持打开,因为我无法验证 PR 文本是否确实被更新。