- html viewer:.html/.htm 沙箱 iframe 预览(相对资源经 /sidebar/html 路径编码路由加载,CSP sandbox + 信任围栏 + cwd 边界),预览/编辑切换 - browser tab:+ 菜单多开(browser:<n>),地址栏后退/前进/刷新/在浏览器中打开(15px 图标),URL 与标题按会话持久化(patchTab + nextBrowser) - 安全:iframe 一律不透明源沙箱(无 allow-same-origin/top-navigation、no-referrer、权限策略全禁、禁 srcdoc);地址栏仅 http(s)、拒绝 localhost 等本机地址;界面实时沙箱状态(关闭红色警示)+ 临时解锁/恢复 - 设置:htmlViewerNoSandbox / htmlViewerDefaultUnsafe / browserNoSandbox / browserInterceptLinks 四个声明式二级设置(默认安全) - 外链拦截:聊天/界面 http(s) 外链默认在侧边栏打开(openTab seed.url),Ctrl/Cmd 点击放行 - 嵌入拒绝面板:browser.probe 路由探测 X-Frame-Options/frame-ancestors,被拒站点显示原因 + 在浏览器中打开/仍然加载 - 设置页可滚动;内置 7 tab + 9 viewer;新增设计文档;268 个测试全绿
71 lines
3.1 KiB
TypeScript
71 lines
3.1 KiB
TypeScript
/**
|
|
* Pure URL vocabulary of the /sidebar/html route (HTML previewer).
|
|
*
|
|
* Why path-encoded parameters instead of a query string: the previewed
|
|
* page resolves its relative assets (./style.css, img/x.png) against the
|
|
* document URL, and the WHATWG URL algorithm DROPS the query of a
|
|
* path-relative reference — `/sidebar/html?a=1&path=/a/b/` + `./style.css`
|
|
* would lose the session scope and the route would reject the asset.
|
|
* Encoding everything into the URL path keeps relative resolution inside
|
|
* the same route with every request self-contained:
|
|
*
|
|
* /sidebar/html/<sessionId>/<absolute-path segments, encodeURIComponent'd>
|
|
* /sidebar/html/S/Users/me/proj/index.html
|
|
* + ./style.css → /sidebar/html/S/Users/me/proj/style.css
|
|
* Windows: C:\Users\me\a.html → /sidebar/html/S/C%3A/Users/me/a.html
|
|
*
|
|
* This module is intentionally dependency-free (no node imports, no wire
|
|
* helpers) so the client bundle can import `encodeHtmlUrl` without tripping
|
|
* the build-time purity gate; the host converts decode failures into
|
|
* SidebarError responses at the route boundary.
|
|
*/
|
|
|
|
/** One decoded route reference. */
|
|
export interface HtmlRouteRef {
|
|
sessionId: string
|
|
/** Absolute file path (leading slash; Windows drives keep their colon). */
|
|
path: string
|
|
}
|
|
|
|
/** Decode outcome: the reference, or a client-error description. */
|
|
export type HtmlDecodeResult =
|
|
| { ok: true; ref: HtmlRouteRef }
|
|
| { ok: false; status: 400 | 404; message: string }
|
|
|
|
/** The route prefix both encoders/decoders agree on. */
|
|
export const HTML_ROUTE_PREFIX = '/sidebar/html/'
|
|
|
|
/** Build the route URL for one absolute file path (client + tests). */
|
|
export function encodeHtmlUrl(sessionId: string, path: string): string {
|
|
const segments = path.split(/[\\/]+/).filter(segment => segment !== '')
|
|
return `${HTML_ROUTE_PREFIX}${encodeURIComponent(sessionId)}/${segments.map(encodeURIComponent).join('/')}`
|
|
}
|
|
|
|
/**
|
|
* Decode a route pathname into the session + absolute file path. Rejects
|
|
* a wrong prefix (404), an empty or double-slash path, malformed percent
|
|
* encoding, and a missing sessionId or file path (400). The caller still
|
|
* must bound the decoded path with requireAbsolute + isWithin(cwd) — a
|
|
* decoded `..` segment resolves outside the cwd and is refused there.
|
|
*/
|
|
export function decodeHtmlUrl(pathname: string): HtmlDecodeResult {
|
|
if (!pathname.startsWith(HTML_ROUTE_PREFIX)) {
|
|
return { ok: false, status: 404, message: 'not an html route' }
|
|
}
|
|
const rest = pathname.slice(HTML_ROUTE_PREFIX.length)
|
|
if (rest === '' || rest.includes('//')) {
|
|
return { ok: false, status: 400, message: 'invalid html route path' }
|
|
}
|
|
let segments: string[]
|
|
try {
|
|
segments = rest.split('/').map(segment => decodeURIComponent(segment))
|
|
} catch {
|
|
return { ok: false, status: 400, message: 'malformed URL encoding' }
|
|
}
|
|
const [sessionId, ...pathSegments] = segments
|
|
if (sessionId === undefined || sessionId === '' || pathSegments.length === 0 || pathSegments.some(segment => segment === '')) {
|
|
return { ok: false, status: 400, message: 'sessionId and file path are required' }
|
|
}
|
|
return { ok: true, ref: { sessionId, path: `/${pathSegments.join('/')}` } }
|
|
}
|