Files
apes-sidebar/src/html-route.ts
T
Menghuan1918 302c20109f feat: 内置 HTML 预览器 + 沙箱浏览器 tab(v0.5.0)
- 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 个测试全绿
2026-08-11 19:28:48 +08:00

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('/')}` } }
}