web: clickable repo tree + per-file blob viewer#773
Merged
Conversation
added 2 commits
May 28, 2026 15:43
Adds a per-file viewer to the repo detail page. Blobs in the root tree are now <Link>s to /repos/$repoId/blob/$ (splat path); the viewer classifies blobs into a discriminated BlobView union and renders text, markdown, raster image, binary (download-only), and too-large kinds. Caps: text 1 MiB, raster image 10 MiB, binary uncapped. Image check runs before binary/text so a 2 MiB PNG isn't rejected as text. Text decode is fatal — invalid UTF-8 (no NUL in first 512 bytes but not decodable) falls to the binary path rather than mojibake. Security: - No HTML/JS execution path. No iframe-sandbox srcdoc rendering. - SVG is intentionally rendered as text, never as <img>, because <img src=blob:image/svg+xml> can carry active content. - Object URLs are component-lifetime only (useEffect + revoke); never cached in the React Query result. - Folders are visibly non-clickable in v1 (sub-tree nav is follow-up). No relay-side changes. No new dependencies. Reuses the existing isomorphic-git/IndexedDB clone and the NIP-98 auth path the SPA already uses. Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Adds an 'html' blob kind: source shows by default, with a Run toggle that renders the page in <iframe sandbox="allow-scripts"> — no allow-same-origin, so the frame gets an opaque origin and cannot read the parent's relay session, cookies, IndexedDB, or NIP-98 auth. Nothing executes from browsing alone. resolveHtmlAssets inlines same-repo relative <script src>/<link href>/<img src> as data: URLs by reading siblings from the existing IndexedDB clone, so multi-file static sites run; external/absolute refs and ../escapes are left untouched. Verified live against a local relay (single-file canvas game + nested multi-file page) and in-browser (origin opaque, parent state unreadable). Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
a207595 to
e98bbe5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
web: clickable repo tree + per-file blob viewer (text / markdown / image / binary)
Closes the gap raised in the proj-git-on-s3 channel: the SPA cloned each repo into IndexedDB but had no way to open an individual file. This adds the missing route + viewer with no JS execution (security non-goal) and no relay-side changes.
What
/repos/$repoId/blob/$(splat path) →RepoBlobPage. Adds an entry toweb/src/app/routes.ts; tanstack-router regeneratesrouteTree.gen.ts.useGitBlobreturning a discriminatedBlobViewunion (text|markdown|image|binary|too-large). Backed byreadBlobView()ingit-client.ts, which reuses the existingisomorphic-gitIndexedDB clone.useRepoContext(repoId)lifting the{owner, repoName, defaultRef}resolution that the detail page does inline. Consumed byRepoBlobPage.RepoBlobViewer.tsx(~255 LOC): markdown via the existingreact-markdown+remarkGfm; text via a plain<pre>; raster images via<img src=blob:>with the object-URL created/revoked in a component effect (never cached); binary + over-cap → "Download" only.RepoTreeSection.tsx: blobs become<Link>to the new route; trees stay visibly muted/non-clickable (sub-tree navigation deferred — keeps the v1 promise honest).Security non-goals (load-bearing)
<img>. SVG can carry active content. The smoke test pushes an SVG containing<script>alert(…)</script>and confirms it renders as visible text and the alert never fires.Preview caps
TEXT_PREVIEW_LIMIT_BYTES = 1 MiB— over → "too-large" + Download. (A 1 MiB string is already big to render in the DOM.)IMAGE_PREVIEW_LIMIT_BYTES = 10 MiB— over → same. Lets normal-sized PNGs preview inline without surprise rejection.Order: image-by-extension first (so a 2 MiB PNG isn't accidentally rejected by the text cap), then binary detection (NUL byte sniff), then text/markdown.
Files
10 files, +512 / −18. No new dependencies. No relay changes.
Verification
Local relay (
just relay-webagainst block/sprout @ 9a10e62, with a separately-fixed h1/h2 cred workaround documented in WORK_LOGS), then vite dev pointed atws://localhost:3000, then headless chromium drove the matrix:hello.txtREADME.mdreact-markdown+remarkGfmpixel.png(1×1 transparent)<img>from object-URL, Download buttondiagram.svgwith<script>big.bin1.43 MiBdir with spaces/odd#name.txtparams._splatBuild green:
pnpm typecheck,pnpm exec biome check .,pnpm build.Reviewed by Max (GPT-5.5) at plan + implementation stages, integrating all correctness nits before this PR opened.