Files
orca/src/shared/doc-preview-scheme.ts
T
Jinwoo Hong cb848647e5 fix(browser-preview): require explicit preview capabilities (STA-5758) (#16921)
* fix(browser-preview): require explicit preview capabilities (STA-5758)

Scope document reads to approved directories, confirm external links before opening them, revoke grants with tab lifecycle, and keep document-preview session state rollback-safe across mixed client/runtime versions.

* Harden document preview lifecycle and permissions

* Document preview DNS prefetch residual

* Make preview E2E guest focus explicit

* fix(browser-preview): entry-file-only authority for root-level docs, contained chip layout, re-issued gate paths (STA-5758)

A grant whose document directory is its own request base — a doc at the
workspace root, or outside any workspace — now reads nothing but the entry
file until the reader approves a directory, at both the lexical and the
canonical containment pass. The DNS-prefetch residual can only beacon what
the page can read, and a root-level document could previously read the
whole worktree silently.

The identity chip's host badge overflowed the chip's layout box under
squeeze (Linux CI): every row member can now shrink and truncate, verified
by a width sweep in isolated Chromium down to ~120px chips.

The Allow banner says what it grants: 'Allow folder', reading files in the
named directory, for the life of the preview.

The reliability-gate manifest command, testFiles entry, assertion refs and
dated evidence naming the deleted doc-preview-external-link-bridge.test.ts
are re-issued at doc-preview-external-link-confirmation.test.ts with a
fresh 189/189 run; the focus-gate assertion text follows the shipped gate.

* fix(browser-preview): hide the chip identity row below 24rem instead of clipping it, ellipsize the host badge, catalog the new i18n keys (STA-5758)

CI's preview pane leaves the chip ~40px: no truncation shows anything
there, so the Workspace-file label and host badge now hide whole below a
24rem container threshold sized so that visible implies contained. The
badge text gains an inner text box — text directly inside the flex pill
clipped both ends with no ellipsis. The e2e geometry oracle asserts
containment when the row shows and the threshold when it does not.

verify:localization-catalog: the hardening's new preview keys (and the
renamed allowDirectory) join en.json via sync:localization-catalog.

* feat(browser-preview): batch blocked folders into one access decision (STA-5758)

Sequential per-folder banners trained the allow reflex without adding
judgment — a reader cannot weigh assets/ against data/. The banner now
accumulates every folder a load surfaces, names them (three, then a
count, full list in the title), and grants exactly that set with one
Allow-N-folders click and one reload. Dismiss fences the whole named
set. The map lives behind a ref with a version tick so a dismissal
fences an offer landing in the same event batch.
2026-08-28 00:42:07 -04:00

111 lines
4.1 KiB
TypeScript

/**
* Wire shape for the local document-preview scheme. The main process answers
* `orca-preview://<grantId>/<relative-path>` by reading the owning workspace's
* disk over the same channels the editor uses, so remote HTML docs render in a
* local webview instead of being routed through the remote-browsing machinery.
*/
export const DOC_PREVIEW_SCHEME = 'orca-preview'
/** Why: non-persistent and its own partition — preview bytes never share storage with user browsing or workspace browser profiles. */
export const DOC_PREVIEW_PARTITION = 'orca-doc-preview'
export const DOC_PREVIEW_MINT_GRANT_CHANNEL = 'docPreview:mintGrant'
export const DOC_PREVIEW_REVOKE_GRANT_CHANNEL = 'docPreview:revokeGrant'
export const DOC_PREVIEW_AUTHORIZE_DIRECTORY_CHANNEL = 'docPreview:authorizeDirectory'
export const DOC_PREVIEW_EXTERNAL_LINK_CHANNEL = 'docPreview:externalLink'
/**
* The preview guest's preload reports a trusted anchor click here. Renderer↔main only — no paired
* client ever sees it, and main gates every report on the sender being a live, grant-bound preview guest.
*/
export const DOC_PREVIEW_LINK_CLICK_CHANNEL = 'docPreview:linkClick'
/** The one out-of-band route from the preview's main-side fences to the shell hosting it. */
export const DOC_PREVIEW_LOAD_FAILURE_CHANNEL = 'docPreview:loadFailure'
/** Why: an unreadable document still answers with a real HTTP status, so the guest paints the
* handler's plain-text body instead of failing to load. The shell needs the reason out-of-band. */
export type DocPreviewFileFailureReason =
| 'authorization-required'
| 'too-large'
| 'unsupported-asset'
| 'unreadable'
export type DocPreviewFileFailure = {
grantId: string
relativePath: string
reason: DocPreviewFileFailureReason
}
/**
* A download the preview partition refused. Why it carries no path: the document names the file it
* offers, and the notice this becomes is Orca's chrome — a payload with a path invites rendering
* page-authored text in the app's own UI, and a path equal to the entry document's would route a
* refused download into the panel that hides the page.
*/
export type DocPreviewDownloadBlocked = {
grantId: string
reason: 'download-blocked'
}
export type DocPreviewFailure = DocPreviewFileFailure | DocPreviewDownloadBlocked
export const DOC_PREVIEW_GRANT_ID_PATTERN = /^[0-9a-f]{32}$/
export function isDocPreviewGrantId(value: string): boolean {
return DOC_PREVIEW_GRANT_ID_PATTERN.test(value)
}
/** Encodes each segment separately so `/` keeps its separator meaning and `#`/`?` cannot split the path. */
export function buildDocPreviewUrl(grantId: string, relativePath: string): string {
const segments = relativePath
.replace(/\\/g, '/')
.split('/')
.filter((segment) => segment.length > 0)
.map((segment) => encodeURIComponent(segment))
return `${DOC_PREVIEW_SCHEME}://${grantId}/${segments.join('/')}`
}
export type DocPreviewUrlTarget = {
grantId: string
/** Slash-joined, percent-decoded path segments; never leading-slashed. */
relativePath: string
}
/**
* Whether a string is, or is trying to look like, a preview URL. Deliberately a prefix test rather
* than a parse: this answers for text a document chose — a title Chromium fell back to, most of
* all — where anything carrying a grant must be refused even when it would not parse.
*/
export function isDocPreviewUrl(candidate: string): boolean {
return candidate.trimStart().toLowerCase().startsWith(`${DOC_PREVIEW_SCHEME}://`)
}
export function parseDocPreviewUrl(rawUrl: string): DocPreviewUrlTarget | null {
let parsed: URL
try {
parsed = new URL(rawUrl)
} catch {
return null
}
if (parsed.protocol !== `${DOC_PREVIEW_SCHEME}:`) {
return null
}
const grantId = parsed.hostname
if (!isDocPreviewGrantId(grantId)) {
return null
}
const segments: string[] = []
for (const rawSegment of parsed.pathname.split('/')) {
if (rawSegment.length === 0) {
continue
}
let segment: string
try {
segment = decodeURIComponent(rawSegment)
} catch {
return null
}
segments.push(segment)
}
return { grantId, relativePath: segments.join('/') }
}